Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの例外処理入門!初心者向けに基礎からわかりやすく解説

先日、Rubyの基本構文は理解しているものの、「例外とは何か」「なぜ便利なのか」がいまひとつピンとこない初心者向けの例外解説記事を探していました。しかし見つからなかったため、自分で書いてみることにしました。この記事が皆さんのお役に立てば幸いです。分かりにくい点があれば、@StarrHorneまでツイートでご連絡ください。

例外(Exception)とは何か?

例外とは、Rubyが予期しない出来事に対処するための仕組みです。

コードにタイプミスがあり、SyntaxErrorNoMethodErrorといったメッセージとともにプログラムがクラッシュした経験はありませんか?それが例外の動作を目撃した瞬間です。

Rubyで例外が発生すると、通常の処理は中断され、プログラムのシャットダウンが始まります。何も対処しなければ、最終的にエラーメッセージを出力してプログラムは終了します。

具体例を見てみましょう。以下のコードではゼロ除算を試みています。これは不可能な操作なので、RubyはZeroDivisionErrorという例外を発生させます。プログラムは終了し、エラーメッセージが出力されます。

1 / 0
# プログラムがクラッシュし、"ZeroDivisionError: divided by 0" と出力される

クラッシュするプログラムはユーザーを怒らせてしまいます。そこで通常は、このシャットダウンを止めて、エラーに対して賢く対応したいと考えるでしょう。

これを「例外のrescue(捕捉)」「ハンドリング」「キャッチ」と呼びます。すべて同じ意味です。Rubyでは次のように書きます。

begin
  # ここで発生した例外は...
  1/0
rescue
  # ...このコードを実行させる
  puts "Got an exception, but I'm responding intelligently!"
  do_something_intelligent()
end

# このプログラムはクラッシュしない。
# "Got an exception, but I'm responding intelligently!" と出力される

例外自体は発生していますが、rescueされたためプログラムはクラッシュしません。終了する代わりに、Rubyはrescueブロック内のコードを実行し、メッセージを出力します。

これは便利ですが、大きな制限が一つあります。「何か問題が起きた」とは教えてくれるものの、何が問題だったのかまでは分からないのです。

問題の詳細情報は、すべて「例外オブジェクト」の中に格納されています。

例外オブジェクト

例外オブジェクトは普通のRubyオブジェクトです。rescueで捕捉した例外についての「何が起きたのか」というデータをすべて保持しています。

例外オブジェクトを取得するには、少し異なるrescue構文を使います。

# すべてのエラーを捕捉し、例外オブジェクトを `e` に代入
rescue => e

# ZeroDivisionError のみを捕捉し、例外オブジェクトを `e` に代入
rescue ZeroDivisionError => e

上記の2つ目の例では、ZeroDivisionErrorが変数eに入るオブジェクトのクラスです。これまで説明してきた例外の「種類」は、実はすべてクラス名にすぎません。

例外オブジェクトにはデバッグに役立つデータも含まれています。ZeroDivisionErrorの例外オブジェクトを見てみましょう。

begin
  # ここで発生した例外は...
  1/0
rescue ZeroDivisionError => e
  puts "Exception Class: #{ e.class.name }"
  puts "Exception Message: #{ e.message }"
  puts "Exception Backtrace: #{ e.backtrace }"
end

# 出力結果:
# Exception Class: ZeroDivisionError
# Exception Message: divided by 0
# Exception Backtrace: ...バックトレースが配列として出力される...

ほとんどのRuby例外と同様に、クラス名に加えてメッセージとバックトレースが含まれています。

自分で例外を発生させる(raise)

ここまでは例外の捕捉について説明してきましたが、自分で例外を発生させることもできます。これを「raise(発生させる)」と呼び、raiseメソッドを呼び出すことで実現します。

自分で例外を発生させる場合、どの種類の例外を使うかを選べますし、エラーメッセージも自由に設定できます。

例を見てみましょう。

begin
  # "You messed up!" というメッセージ付きの ArgumentError を発生させる
  raise ArgumentError.new("You messed up!")
rescue ArgumentError => e
  puts e.message
end

# 出力結果: You messed up!

ご覧のとおり、カスタムメッセージ("You messed up!")を持つ新しいエラーオブジェクト(ArgumentError)を作成し、raiseメソッドに渡しています。

Rubyらしく、raiseはいくつかの書き方で呼び出せます。

# 明示的で筆者のお気に入りの書き方
raise RuntimeError.new("You messed up!")

# ...同じ結果になる
raise RuntimeError, "You messed up!"

# ...同じ結果になる。ただしこの書き方で発生できるのは RuntimeError のみ
raise "You messed up!"

カスタム例外を作る

Rubyの組み込み例外は優秀ですが、あらゆるユースケースをカバーしているわけではありません。

たとえばユーザー管理システムを構築していて、ユーザーがアクセス権限のない領域にアクセスしようとしたときに例外を発生させたい場合はどうでしょうか?Rubyの標準例外には適切なものがないため、新しい種類の例外を作成するのがベストです。

カスタム例外を作るには、StandardErrorを継承した新しいクラスを作るだけです。

class PermissionDeniedError < StandardError

end

raise PermissionDeniedError.new()

これは普通のRubyクラスです。つまり、他のクラスと同様にメソッドやデータを追加できます。「action」という属性を追加してみましょう。

class PermissionDeniedError < StandardError

  attr_reader :action

  def initialize(message, action)
    # 親のコンストラクタを呼び出してメッセージを設定
    super(message)

    # action をインスタンス変数に保存
    @action = action
  end

end

# ユーザーが削除権限のないものを削除しようとしたときなどに、
# 次のように使える:
raise PermissionDeniedError.new("Permission Denied", :delete)

例外のクラス階層

先ほどStandardErrorを継承してカスタム例外を作りましたが、実はStandardError自身もExceptionを継承しています。

実際、Rubyの任意の例外のクラス階層をたどると、必ずExceptionに行き着きます。証明してみましょう。以下はRubyの主な組み込み例外を階層的に表示したものです。

Exception
 NoMemoryError
 ScriptError
   LoadError
   NotImplementedError
   SyntaxError
 SignalException
   Interrupt
 StandardError
   ArgumentError
   IOError
     EOFError
   IndexError
   LocalJumpError
   NameError
     NoMethodError
   RangeError
     FloatDomainError
   RegexpError
   RuntimeError
   SecurityError
   SystemCallError
   SystemStackError
   ThreadError
   TypeError
   ZeroDivisionError
 SystemExit

もちろん、これらすべてを暗記する必要はありません。お見せしたのは、この階層構造の考え方が特定の理由で非常に重要だからです。

特定のクラスのエラーをrescueすると、その子クラスのエラーも一緒に捕捉されます。

もう一度強調します。

rescue StandardErrorと書くと、StandardErrorクラスの例外だけでなく、その子クラスの例外も捕捉します。チャートを見ると分かるように、対象はかなり多く、ArgumentErrorIOErrorなどが含まれます。

もしrescue Exceptionと書くと、すべての例外を捕捉することになり、これは非常に危険な考え方です。

すべての例外を捕捉する(悪いやり方)

叱られたいなら、Stack Overflowに次のようなコードを投稿してみてください。

# 絶対にやってはいけない例
begin
  do_something()
rescue Exception => e
  ...
end

上記のコードはすべての例外を捕捉します。絶対に真似しないでください!プログラムを奇妙な形で壊してしまいます。

というのも、Rubyはエラー以外の目的でも例外を使用しているからです。OSからのメッセージである「シグナル」の処理にも例外が使われています。「Ctrl-C」を押してプログラムを終了したことがあれば、それはシグナルを使ったことになります。すべての例外を抑制すると、こうしたシグナルも一緒に抑え込んでしまうのです。

また、構文エラーのように、本来はプログラムをクラッシュさせるべき例外もあります。これらを抑制すると、タイプミスなどのミスに永遠に気づけなくなってしまいます。

すべてのエラーを捕捉する(正しいやり方)

クラス階層のチャートに戻って見ると、捕捉したいエラーはすべてStandardErrorの子クラスであることが分かります。

つまり、「すべてのエラー」を捕捉したい場合は、StandardErrorをrescueすればよいのです。

begin
  do_something()
rescue StandardError => e
  # アプリケーションの例外だけが捕捉される。
  # SyntaxError などはそのまま素通りする。
end

実際、例外クラスを指定しなかった場合、RubyはStandardErrorを指定したものとみなします。

begin
  do_something()
rescue => e
  # これは StandardError を rescue するのと同じ意味
end

特定のエラーを捕捉する(ベストなやり方)

すべてのエラーを捕捉する方法が分かったところで、実はそれは通常あまり良い考えではないことも知っておきましょう。コードの臭い(code smell)とされ、有害と見なされることが多いのです。

それは通常、「どの具体的な例外を捕捉すべきか」を調べるのが面倒だったサインです。そして、ほぼ確実に後でしっぺ返しを受けることになります。

時間をかけて正しく書きましょう。具体的な例外を捕捉してください。

begin
  do_something()
rescue Errno::ETIMEDOUT => e
  # Errno::ETIMEDOUT 例外のみを捕捉する
end

さらに、同じrescueブロックで複数の種類の例外を捕捉することもできます。言い訳はできませんね。

begin
  do_something()
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
end
  1. Rubyで例外にコンテキストデータを追加する3つの方法

    標準のバックトレースやエラーメッセージだけでは、エラーの原因を特定するのに十分な情報が得られないことがあります。そんなときに役立つのが、例外への追加データの付与です。幸い、Rubyではこれをとても簡単に実現できます。 エラーメッセージをカスタマイズする 例外にコンテキスト情報を加える最もシンプルな方法は、メッセージそのものに情報を組み込んでしまうことです。次の例では、発生した例外を捕捉し、新しいメッセージを付けて再送出しています。 begin raise foo rescue => e raise e.class, bar end # RuntimeError: bar

  2. Rubyで実践する関数型プログラミング完全ガイド ― 純粋関数・イミュータブルデータ・カリー化の基本

    Rubyを書いていると、「関数型プログラミング」という言葉を目にする機会が増えてきます。しかし、実際にどんなものなのか、自分のコードに取り入れるべきなのか、疑問に感じている方も多いのではないでしょうか。 関数型プログラミングとは、具体的に何を指すのか? オブジェクト指向プログラミング(OOP)とは何が違うのか? Rubyでも関数型的な書き方を採用すべきなのか? この記事では、これらの疑問にわかりやすく答えながら、Rubyで今日から使える関数型プログラミングの考え方とテクニックを解説します。 関数型プログラミングとは? 関数型プログラミングは一時的な流行や難しい専門用語ではなく、長い歴史を持