这是ruby小技巧系列的第三部分,这些技巧是我们从过去两年的实战经验中所收获的。第一部分涵盖了代码块(blocks)和范围对象(ranges),第二部分讨论了拆分重构以(destructuring)及类型转换(type conversions)。
异常(Exceptions)
处理异常是富有技巧性的,你非常容易掉入自己挖的坑内并且难以走出。下面有一下规则可以借鉴,它会让你的代码更加容易调试,虽然你得付出一些小小的努力,但是你会得到很大的回报。
首先,不要使用rescue修饰符。这里有一个例子,当你在获取数组中的一个元素时,如果发生发生错误,将返回nil。
1 2 3 |
def safe_fetch(array, index) array[index] rescue nil end |
但是我们不知道它隐藏了怎样的错误。参数array有可能为nil,参数index可能为nil,参数index有可能为string,你有可能将参数array的名字写错,或者任何你可能想象到的任何错误。
这意味着我们不能从阅读代码得到代码的意图,同时意料之外的错误不可能展现出来。
但是,如果在执行时输入nil将会发生意料之外的错误。这种情况下,我们很难发现导致错误的原因是错误的传入了nil。
使用标准形式的rescue可以减少你很多痛苦,通过捕获我们期望的异常类型,或者是输出异常信息。
1 2 3 4 5 |
def safe_fetch(array, index) array[index] rescue NoMethodError nil end |
1 2 3 4 5 6 |
def safe_fetch(array, index) array[index] rescue => e @logger.debug(e) if @logger nil end |
rescue => e
是rescue StandardError => e
的简写形式,就像单独的rescue
是rescue StandardError
的简写形式。这意味着只能捕获StandardError或StandardError的子类(subclasses)。通常,你应该按照Ruby的惯例,只捕获StandardError以及它的子类(subclasses),永远不要去捕获Exception。捕获Exception意味着你将捕获到你不想要去捕获的异常,例如SystemExit,当你的程序请求退出的时候它将被抛出(raise)。如果你捕获这个异常,那么当你结束程序的时候将不能正常退出。
同样的,当你抛出异常的时候应该抛出StandardError的子类(subclasses),否则你的异常将不能被异常处理机制正确的处理。
在你的项目中定义一种通用的错误类是一种很好的做法,这种做法在gem中也得到了很好的应用,其他的错误类从通用的错误类继承而来。
1 2 3 4 5 6 7 8 9 10 |
module MyProject class Error < StandardError end class NotFoundError < Error end class PermissionError < Error end end |
通过这种方式,你可以使用rescue MyProject::Error
捕获所有的异常,或者你可以指定特定的异常。
一种更加友好,更加易于阅读异常列表的方式是利用class.new
方法来定义异常。Class.new
通过继承作为参数的类来定义一个新类。生成的新类赋给一个常量时,常量将作为类的名称。
1 2 3 4 5 |
module MyProject Error = Class.new(StandardError) NotFoundError = Class.new(Error) PermissionError = Class.new(Error) end |
使用这种方式你要面对的一个问题是,并不是在你代码中所产生的所有异常都是通过代码直接抛出的。例如,当你进行HTTP请求的时候,可能在连接的时候抛出异常。
现在,你可以在代码中捕获这些异常,同时抛出你自己定义类型的异常。但是,你会丢失有价值的调试信息:产生异常异常的原始类,异常信息和调用堆栈。
Ruby允许你实现一个类(class)或模块(module)应用于rescue
关键字作为期待的异常类型。rescue
关键字使用case
相等性操作符#===
去比价传递的参数和期待的异常类型。对于类(class),当参数是异常类型的实例对象或者子类的实例对象则返回true 。对于模块(module),当参数混入(include)了模块或者参数扩展(extend)了模块则返回true。
因此,如果你将基本异常定义为一个模块(module),同时将它混入(include)到特定的异常类中,这些异常类将具有像上面所讲到的异常类的行为。你也可以将代码中引发异常的任意异常类通过扩展基本异常(extend)打上“标签”,这样这些异常就可以通过rescue
关键字和基本异常进行捕获,同时跟踪它的原始异常类,异常信息以及调用堆栈。(你将不能引发基本异常,但这通常是一种很好的做法,你应该抛出特定的异常类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
module MyProject Error = Module.new class NotFoundError < StandardError include Error end class PermissionError < StandardError include Error end def get(path) response = do_get(path) raise NotFoundError, "#{path} not found" if response.code == "404" response.body rescue SocketError => e e.extend(Error) raise e end end ... begin get("/example") rescue MyProject::Error => e e #=> MyProject::NotFoundError or SocketError end |
这种方式的另一种用法是作为两种不同服务的适配器类(例如数据库客户端),映射不同种类的异常到相同的分类。因此,当在适配器下切换不同的数据库客户端后,rescue
仍然工作。
然而这种技术有一个小的缺点。#extend
方法调用将会使Ruby的全局方法缓存失效,在每次调用的时候会降低程序的速度。我们将会在Ruby2.1中得到一个更加优秀的方法缓存失效,让我们不用去担心任何事情。但即使是现在这仍是一项有效和强大的技术。
模块(Module)
模块在Ruby中有多种用途。或许最常见的就是作为命名空间(name-spacing),另外一种主要的用途是作为混入(mixin)。
像Enumerable和Comparabel一样的模块是非常神奇的,可以很容易的让你的类增加复杂的行为,通过在类中定义一些简单的方法同时混入(mixin)这些模块。另外,模块也拥有一些其他的用途。
有时候,你有一系列的方法非常接近函数,它们接受输入,返回结果,它们不会对当前对象self做任何操作。模块可以作为一种很好的方式可以将这些方法集中在一起。
这里有一个在我们项目中的例子(这里还有一些其他更多的方法,由于特殊原因不太适合和大家分享)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
module Geo module_function RADIUS_OF_THE_EARTH = 6371 def distance((origin_lat, origin_long), (dest_lat, dest_long)) return unless origin_lat && origin_long && dest_lat && dest_long sin_lats = Math.sin(rad(origin_lat)) * Math.sin(rad(dest_lat)) cos_lats = Math.cos(rad(origin_lat)) * Math.cos(rad(dest_lat)) cos_longs = Math.cos(rad(dest_long) - rad(origin_long)) x = sin_lats + (cos_lats * cos_longs) x = [x, 1.0].min x = [x, -1.0].max Math.acos(x) * RADIUS_OF_THE_EARTH end def rad(degree) degree.to_f / (180 / Math::PI) end def degree(rad) rad.to_f * (180 / Math::PI) end def miles(km) km / 1.609344 end def km(miles) miles * 1.609344 end end |
在模块开始的module_function
声明是一个方法可见性修饰符,像public
,private
,protected
。它使方法直接调用时成为模块的类方法(class method),当模块被混入时成为一个私有实例方法(private instance method)。
1 2 3 4 |
Geo.distance([51.47872, -0.610248], [51.5073346 , -0.1276831]) #=> 33.55959095208182 include Geo distance([51.47872, -0.610248], [51.5073346 , -0.1276831]) #=> 33.55959095208182 |
将module_function
改为extend self
可以得到相同的效果。
1 2 3 4 |
module Geo extend self ... end |
这里,模块的实例方法也同时作为类方法。不同的是你可以使用其他方法可见性修饰符,使方法在模块中为私有(private)的,在实例方法中为公有(public)的(当使用module_function
修饰符时,所有的方法作为模块为公有的,作为实例方法为私有的)。
模块对于处理单例对象来说也是非常方便的,你不用去浪费时间去阻止多余一个对象被创建,或者考虑怎样去获取引用,在Ruby中这些都是内置的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
require "net/http" module APIClient @ http = Net:HTTP.new("example.com", 80) @ user = "user" @ pass = "pass" def self.get(path) request = Net::HTTP::Get.new(path) request.basic_auth(@ user, @ pass) @ http.request end end response = APIClient.get("/api/examples") |
让我们进入第四部分。