理想の世界では、すべてが完璧に動きます。 しかし現実の世界では、物事がうまくいかないこともあり、こうした状況にどう対処するかが、ソフトウェアを堅牢で信頼できるものにするために重要になります。 例外は、エラーや予期しない状況にうまく対処するための、プログラミングにおける重要な概念です。
エラーを発生させても、それを処理しなければ、プログラムは停止し、エラーメッセージを出力します。 ほとんどの場合、エラーが起きたときにプログラムを停止させたくはありません。むしろ、エラーを処理してプログラムの実行を続けたいはずです。
Crystalでは、例外はraiseキーワードを使って発生させます。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を捕まえ、2番目の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
メソッドの中には、!が付くものと付かないものの2つの版があるものがあります。
これには2つの意味がありえます。1つはメソッドがオブジェクトを変更すること、もう1つはメソッドが例外を発生させる可能性があることです。
ただし、真偽値の概念で触れた、メソッド名を?で終えるという別の慣習もあります。
メソッドの中には、デフォルトでは例外を発生させますが、?で終わる版もあり、そちらは例外を発生させる代わりに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
コードを書いていく中で、エラー処理を改善し、より堅牢にして、農場での日々の生活で使うときに後からデバッグしやすくします。
まずは、牛1頭あたりの餌の量を計算するために必要なコードの作成に取り組みます。
FodderCalculatorオブジェクトと、牛の頭数を表す整数を引数に取るメソッドTheFarm.divide_foodを実装しましょう。
このタスクでは、渡される牛の頭数は常に0より大きいと仮定します。
このメソッドは、牛1頭あたりの餌の量を返すようにします。
計算するには、まずすべての牛の餌の合計量を取得する必要があります。
これは、fodder_amount!メソッドを呼び出し、牛の頭数を渡すことで行います。
さらに、この量に掛け合わせる係数も必要です。
この係数は、fattening_factor!メソッドを呼び出すことで取得します。
これら2つの値と牛の頭数がそろえば、牛1頭あたりの餌の量を(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.divide_foodと同じシグネチャを持つ関数TheFarm.validate_input_and_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}
カスタムエラーを手に入れたら、牛の頭数を整数として受け取り、エラー(またはnil)を返す関数TheFarm.validate_number_of_cowsを実装しましょう。
"there are no negative cows"に設定したInvalidCowsErrorを返します。"no cows don't need food"に設定したInvalidCowsErrorを返します。nilを返します。TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
この検証関数を設定するという大変な作業を終えたあと、もう夕方になっていることに気づき、山の上に沈む夕日を楽しむために机を離れます。 新しい検証関数を実際に自分のコードに追加する作業は、また別の日に回すことにします。