在理想的世界裡,一切都能完美運作。 但在現實世界中,事情可能出錯,而我們如何處理這些情況,對於確保軟體穩健可靠至關重要。 例外是程式設計中極為重要的概念,讓我們能夠優雅地處理錯誤和突發狀況。
拋出錯誤後若未加以處理,程式會停止並拋出錯誤訊息。 大多數情況下,你不會希望程式一出錯就停止,而是希望處理錯誤,讓程式繼續執行下去。
在 Crystal 中,例外是使用raise關鍵字拋出的,可以傳入String或Exception物件。
若未處理,程式會停止並印出錯誤訊息。
raise "This is an error"
Crystal 內建了許多例外,例如ArgumentError、IndexError、KeyError、IOError、SystemCallError、TypeError、ZeroDivisionError等等。
這些例外都需要你傳入一則訊息。
raise ArgumentError.new("This is an argument error")
我們不會希望程式在拋出例外時崩潰。
因此,當我們知道某段程式碼容易出錯時,可以用begin區塊把它包起來,並用rescue區塊捕捉例外。
begin區塊標示出可能拋出例外的程式碼開頭,而rescue區塊則負責處理例外。
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
rescue區塊也可以指定一個變數,用來取得例外物件。
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
rescue區塊也可以指定特定的例外型別,只捕捉該種例外。
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end
# or
begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
你可以使用多個rescue區塊來處理不同型別的例外。
在下面的範例中,第一個rescue區塊會捕捉ArgumentError,第二個rescue區塊則會捕捉其他任何例外。
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
begin區塊也可以搭配else區塊,只有在沒有拋出例外時才會執行。
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
最後,還有ensure區塊,無論是否拋出例外,它一律都會執行。
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
有些方法有兩個版本,一個結尾帶!,另一個則沒有。
這可能代表兩種不同的意思:一種是該方法會修改物件,另一種是該方法可能拋出例外。
不過,在布林概念裡也提過另一個慣例,就是在方法結尾加上?。
有些方法預設會拋出例外,但同時也有一個以?結尾的版本,會回傳nil,而不是拋出例外。
當你想避免拋出錯誤時,這種做法就很理想。 由於不必建立堆疊追蹤,這對效能也有幫助,而且只要設定得當,還能讓程式碼更安全。
你也可以繼承Exception類別來建立自己的例外。
這麼做時,你可以選擇覆寫initialize方法來設定例外的訊息。
只要把訊息指定給名為@message的實體變數即可。
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
你等了很久的那一天終於到來,現在你成了阿爾卑斯山上美麗農場的驕傲主人。
你依然不喜歡一大早就起床餵牛。
因為你是個優秀的工程師,你打造了一台食物分配器,FEED-M-ALL。
完成這個專案的最後一件事,是一段能計算每頭牛該分到多少飼料的程式碼。 每頭牛分到的份量必須相同,這很重要,你得避免衝突。 牛可是很敏感的喔。
幸運的是,你不需要自己推導所有計算飼料份量的公式。 你用了一個在網路上找到的神祕外部函式庫。 據說它能讓牛群過得最快樂。 這個函式庫公開了一個符合以下介面的類別。 你自己寫的程式碼將會依賴它。
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
在你開發程式碼的過程中,你會改善錯誤處理,讓它更強健,之後在農場日常生活中使用時也更容易除錯。
首先,你專注在撰寫計算每頭牛飼料份量所需的程式碼。
實作一個方法 TheFarm.divide_food,它接受一個 FodderCalculator 物件,以及一個整數的牛隻數量作為引數。
在這項任務中,假設傳入的牛隻數量永遠大於零。
這個方法應該回傳每頭牛的食物份量。
要進行計算,你首先需要取得所有牛隻的飼料總量。
這要透過呼叫 fodder_amount! 方法並傳入牛隻數量來完成。
此外,你需要一個用來乘上這個總量的係數。
這個係數可以透過呼叫 fattening_factor! 方法取得。
有了這兩個值和牛隻數量,你現在就能計算出每頭牛的食物份量(型別為 float64)。
如果你呼叫的其中一個方法回傳錯誤,執行就應該停止並回傳 0。
fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0
fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0
在進行上面第一項任務時,你發現自己使用的外部函式庫品質不如預期。 例如,它無法妥善處理無效的輸入。 你想在自己的程式碼裡加入輸入值的檢查,來繞過這個限制。
撰寫一個函式 TheFarm.validate_input_and_divide_food,它的簽章和上面的 TheFarm.divide_food 相同。
TheFarm.divide_food 並回傳該呼叫的結果。"Number of cows must be greater than 0"。TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15
TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0
在往下傳之前先檢查牛隻數量是個好做法,但你對這個不夠具體的錯誤訊息不太滿意。
你決定做得更好,建立一個名為 InvalidCowsError 的自訂錯誤類別。
這個自訂錯誤應該保存牛隻數量(Int32)和一段自訂訊息(String),而 Expection 方法應該將資料序列化成以下格式:
{number of cows} cows are invalid: {custom message}
有了這個自訂錯誤後,實作一個函式 TheFarm.validate_number_of_cows,它接受整數的牛隻數量,並回傳一個錯誤(或 nil)。
InvalidCowsError,並將自訂訊息設為 "there are no negative cows"。InvalidCowsError,並將自訂訊息設為 "no cows don't need food"。nil,表示驗證成功。TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
辛苦建立好這個驗證函式之後,你發現已經是傍晚了,便離開書桌去享受山巒上的日落。 你把真正把新的驗證函式加進程式碼這件工作留到改天再說。