跳至內容

例外處理

Crystal 處理錯誤的方式是透過拋出和捕獲例外。

拋出例外

您可以使用呼叫頂層的 raise 方法來拋出例外。與其他關鍵字不同,raise 是一個具有兩個多載的常規方法:一個接受 String,另一個接受 Exception 實例

raise "OH NO!"
raise Exception.new("Some error")

String 版本只會建立一個帶有該訊息的新的 Exception 實例。

只有 Exception 實例或子類別才能被拋出。

定義自訂例外

要定義自訂的例外型別,只需從 Exception 繼承即可

class MyException < Exception
end

class MyOtherException < Exception
end

您可以像往常一樣,為您的例外定義建構子,或者只使用預設的建構子。

捕獲例外

要捕獲任何例外,請使用 begin ... rescue ... end 表達式

begin
  raise "OH NO!"
rescue
  puts "Rescued!"
end

# Output: Rescued!

要存取捕獲的例外,您可以在 rescue 子句中指定一個變數

begin
  raise "OH NO!"
rescue ex
  puts ex.message
end

# Output: OH NO!

要僅捕獲一種例外型別(或其任何子類別)

begin
  raise MyException.new("OH NO!")
rescue MyException
  puts "Rescued MyException"
end

# Output: Rescued MyException

有效的型別限制是 ::Exception 的子類別、模組型別和這些的聯合。

要存取它,請使用類似於型別限制的語法

begin
  raise MyException.new("OH NO!")
rescue ex : MyException
  puts "Rescued MyException: #{ex.message}"
end

# Output: Rescued MyException: OH NO!

可以指定多個 rescue 子句

begin
  # ...
rescue ex1 : MyException
  # only MyException...
rescue ex2 : MyOtherException
  # only MyOtherException...
rescue
  # any other kind of exception
end

您也可以透過指定聯合型別來同時捕獲多種例外型別

begin
  # ...
rescue ex : MyException | MyOtherException
  # only MyException or MyOtherException
rescue
  # any other kind of exception
end

else

只有在沒有捕獲任何例外的情況下,才會執行 else 子句

begin
  something_dangerous
rescue
  # execute this if an exception is raised
else
  # execute this if an exception isn't raised
end

只有在至少指定一個 rescue 子句時,才能指定 else 子句。

ensure

無論是否拋出例外,都會在 begin ... endbegin ... rescue ... end 表達式結束時執行 ensure 子句

begin
  something_dangerous
ensure
  puts "Cleanup..."
end

# Will print "Cleanup..." after invoking something_dangerous,
# regardless of whether it raised or not

或者

begin
  something_dangerous
rescue
  # ...
else
  # ...
ensure
  # this will always be executed
end

ensure 子句通常用於清理、釋放資源等。

簡短語法形式

例外處理有一種簡短的語法形式:假設方法或區塊定義是一個隱含的 begin ... end 表達式,然後指定 rescueelseensure 子句

def some_method
  something_dangerous
rescue
  # execute if an exception is raised
end

# The above is the same as:
def some_method
  begin
    something_dangerous
  rescue
    # execute if an exception is raised
  end
end

帶有 ensure

def some_method
  something_dangerous
ensure
  # always execute this
end

# The above is the same as:
def some_method
  begin
    something_dangerous
  ensure
    # always execute this
  end
end

# Similarly, the shorthand also works with blocks:
(1..10).each do |n|
  # potentially dangerous operation


rescue
  # ..
else
  # ..
ensure
  # ..
end

型別推斷

在例外處理器的 begin 部分內宣告的變數,在 rescueensure 主體內考慮時也會取得 Nil 型別。例如

begin
  a = something_dangerous_that_returns_Int32
ensure
  puts a + 1 # error, undefined method '+' for Nil
end

即使 something_dangerous_that_returns_Int32 從未拋出,或者 a 被賦值,然後執行可能會拋出的方法,也會發生上述情況

begin
  a = 1
  something_dangerous
ensure
  puts a + 1 # error, undefined method '+' for Nil
end

儘管顯然 a 將始終被賦值,但編譯器仍會認為 a 可能從未有機會被初始化。即使這種邏輯未來可能會有所改進,但現在它會迫使您將例外處理器保持在必要的最小值,使程式碼的意圖更加清晰

# Clearer than the above: `a` doesn't need
# to be in the exception handling code.
a = 1
begin
  something_dangerous
ensure
  puts a + 1 # works
end

處理錯誤的其他方法

儘管例外可用作處理錯誤的機制之一,但它們不是您唯一的選擇。拋出例外涉及分配記憶體,並且執行例外處理器通常很慢。

標準函式庫通常提供幾種方法來完成某些事情:一種會拋出,另一種會回傳 nil。例如

array = [1, 2, 3]
array[4]  # raises because of IndexError
array[4]? # returns nil because of index out of bounds

通常的慣例是提供一個替代的「問題」方法來表示此方法的變體回傳 nil 而不是拋出。這讓使用者可以選擇是要處理例外還是 nil。然而,請注意,這並非適用於所有方法,因為例外仍然是首選方式,因為它們不會用錯誤處理邏輯污染程式碼。