這是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") |
讓我們進入第四部分。