今日は、Rubyの例外処理、rescueについて解説します。
Rubyでのプログラミングでは欠かせない、例外処理。
そもそもプログラミングにおける、例外処理とは何でしょうか?
普段、例外を意識しながらのプログラミングはできていますか?
この記事では、rescueの使い方について以下の内容を解説していきます。
【基礎】例外の種類
【基礎】例外を処理するbegin ~ rescueメソッド
【発展】例外の有無に関わらず実行するensure
【発展】処理をもう一度やり直すretry
【発展】意図的に例外を発生させるraise
【発展】File.openで考慮すべき例外
この記事ではRubyで例外処理を行う際に必須となるrescueメソッドの基礎や、raise・ensure・retryそれぞれのメソッドを説明しながら例外処理についてまとめています。
Rubyの例外処理、rescueの使い方をよく把握して自分のスキルとしていきましょう!
そもそも例外とは
そもそも例外とは何でしょうか?
簡単に説明しますと、例外とは「プログラム実行中のエラー」のことを指します。
プログラミングではさまざまな種類のエラーがあります。例えば以下のようなエラーです。
- システムエラー
- サーバー落ち
- ヒューマンエラー
- 入力の失敗
- コードエラー
- ;がない
上記の中でどれが例外になるのでしょう?
システムエラーやヒューマンエラーによって引き起こされたコードエラーやそもそものコードエラーが例外となります。
要するに原因は色々ありますが、コード内で起きたエラー = 例外と考えてよいでしょう。
Rubyで例外を捕捉するためのメソッドはrescueです。
rescueメソッドを使えば、例外が発生した時にどう対処するかあらかじめ設定しておくことができます。
でも、なぜそもそも例外が発生したときにrescueで対処する必要があるのでしょうか?
答えは簡単です。
例外処理を書いておかないと、例外が発生した時点でプログラムが停止してしまい、それ以降に記述してあるコードが読み込まれないからです。
そうなると、もし処理に必要なコードが例外発生より後に書いてあった場合にはプログラムが破綻してしまいます。
なので、例外が発生した時にどういう処理をするのかを設定する必要があるのです。
例外の種類
例外にも色々な種類があります。
- 0で除算したときのZeroDivisionError
- メモリ確保に失敗したときに発生するNoMemoryError
- 予想されていた引数の数よりも少ないときに発生するArgumentError
- 定義されていないローカル変数や定数を使用したときに発生するNameError
などがあります。
Rubyにはそのほかにもたくさんの例外が設定されています。
こちらのRubyのドキュメントに網羅してあるので、参考にしてください。
Ruby 2.1.0 リファレンスマニュアル > ライブラリ一覧 > 組み込みライブラリ
例外の種類と、その意味を知っていると例外が発生した時に、コードのどの部分がおかしいかすぐに気づけるので、基本的な例外の名前を見たら、その意味がわかるようにしておきましょう。
例外を処理するbegin ~ rescueメソッド
基本的なrescue
ここではrescueメソッドの基本的な使い方を見ていきましょう。
begin 1/0 rescue puts "0で割ってはダメです!" end
[実行結果]
0で割ってはダメです!
このプログラムでは、「1/0」の箇所で、整数は0では割れませんので、「ZeroDivisionError」という例外が発生します。
beginブロックで例外が発生すると、beginブロックの処理は中断され、処理のフローはrescueブロックに移ります。
そして、ここでは、resucueブロックで、“0で割ってはダメです!”と表示しています。
その後、bigenブロックへは復帰せず、endまで処理が進んでいます。
例外の捕捉
rescueでは、具体的にどんなエラーが起きたのか、その種類も捕捉することができます。
以下の例では、エラーが発生した後に、例外を捕捉し、rescueメソッドでその例外の種類をチェックしています。
begin 1/0 rescue => e puts e end
[実行結果]
divided by 0
上記コードを実行すると、変数eに格納された例外がなぜ起きたのか教えてくれます。
この場合はどの例外を処理するは省略されていますが、厳密にはStandardErrorクラスのサブクラスにあたる例外だけを捕捉します。
ZeroDivisionErrorクラスはStandardErrorクラスを継承しているので、この場合は例外を捕捉することができます。
StandardErrorのサブクラスは先ほど紹介した例外処理の一覧から確認できます。
つまり、上記のコードを省略せずに書くと以下のコードと同じです。
begin 1/0 rescue StandardError => e puts e end
[実行結果]
divided by 0
また、eには例外オブジェクト(ZeroDivisionErrorなど)が代入されるので、それら例外オブジェクトに実装されている基本的なメソッドを使用することができます。
begin 1/0 rescue StandardError => e puts e puts e.class puts e.class.superclass puts e.message end
[実行結果]
divided by 0 ZeroDivisionError StandardError divided by 0
このように、eだけを出力した場合は、実はe.messageの場合と同じです。
また、class.superclassのようなチェーンメソッドで例外オブジェクトの親クラスを確かめることができます。
このように、例外をresucueで受け取ってから、その例外の種類を後から調べ、それぞれの例外に適切な処理を施すことが可能です。
その例外処理で、状態が完全に復元した場合には(後述します)retryを使用し、beginから処理を再開しても良くなります。
また、致命的な例外だった場合は、最低限の処理(ファイルのクローズやメモリの解放など)を(後述します)ensureブロックで行い、ログにその旨を記載してから、begin~endを終了するという方法もあります。
当たり前のようにクラスの概念を用いて説明していますが、もしもクラスについて理解が浅い場合はこちらの記事を参考にしてください。
例外の種類を分ける
エラーの種類に応じて例外処理を変えることも可能です。
begin 1/0 rescue ZeroDivisionError => e puts e.class puts e.message end
[実行結果]
ZeroDivisionError divided by 0
上記サンプルコードでは、ZeroDivisionErrorが発生したときに行う例外処理を記述しています。
それ以外の例外は捕捉されません。
また、例外は複数引数に渡すことができます。
def fullname(surname, firstname) "#{surname} #{firstname}" end begin 1/3 puts fullname("長瀬") rescue ArgumentError, ZeroDivisionError => e puts e.class puts e.message end
[実行結果]
ArgumentError wrong number of arguments (given 1, expected 2)
このように、ArgumentErrorで捕捉してみました。
ArgumentErrorは期待されている引数の数よりも実際に渡された引数が少ないときに発生します。
例外の応用
ここでは、以下の内容について解説していきます。
- 例外の有無に関わらず実行するensure
- 処理をもう一度やり直すretry
- 意図的に例外を発生させるraise
- File.openで考慮すべき例外
例外の有無に関わらず実行するensure
ensureメソッドは例外の有無に関わらず実行されます。
begin 1/0 rescue => e puts e ensure puts "ensure" end
[実行結果]
divided by 0 ensure
上記サンプルコードでは、0除算の例外が起きた場合でもensureメソッドが実行され、「ensure」と出力されます。
例外が発生しないときのサンプルコードは以下です。
例外が発生しない場合でも、ensure句のブロックは実行されます。
begin 1/1 rescue => e puts e ensure puts "ensure" end
[実行結果]
ensure
例外が発生しなくてもensureメソッドが実行されたことが確認できます。
処理をもう一度やり直すretry
retryメソッドは、beginからの実行を、もう一度やり直す際に使うメソッドです。
beginブロックで例外が発生し、rescueブロックへ処理のフローが移ります。
その際、rescueブロックで、元のエラーの状況が改善し、再びbeginブロックの処理を行うことができるようになった場合、retryメソッドを使って、処理のフローを再びbeginブロックの先頭に移します。
count = 0 begin 1/0 rescue p count += 1 retry if count < 5 puts "無理でした" end
[実行結果]
1 2 3 4 5 無理でした
上記サンプルコードでは、0除算の例外が発生したときのcountが5以上であれば、「無理でした」と出力しbegin~endブロックを抜ける処理を行っています。
無限に繰り返されても処理の無駄なので、リセット回数を設定する必要があります。
ここでは、変数countをカウントアップし、5回以上rescue句へ例外が入った場合、retryするのを止め、”無理でした”と表示し、begin~endの最後のendまで到達し、処理を終えています。
意図的に例外を発生させるraise
raiseメソッドは意図的に例外を起こすためのメソッドです。
begin raise rescue p "例外です" end
上記サンプルコードを実行すると、raiseメソッドにより例外が引き起こされ、rescueメソッドが実行され、「例外です」と出力されます。
また、rescueメソッドでは意図的に引き起こす例外の種類を引数に渡すことで設定できます。
begin raise ZeroDivisionError rescue => e p e.class end
[実行結果]
ZeroDivisionError
このように、ZeroDivisionErrorの例外を発生させた後に、rescueで捕捉し、どの例外かを出力しています。
File.openで考慮すべき例外
最後によく使用するFile.openメソッドで起きる例外も見てきましょう。
サンプルコードは以下のようになります。
ファイル“sample.txt”ファイルを読み込んで、1行ずつ表示しています。
rescueで捕捉する例外は、例外の分類的に細かい種類の例外を先に書いています。
ここでは、SystemCallErrorを、より一般的で広範囲なIOErrorよりも先に書いています。
もし、広範囲な例外が先頭に列挙してあると、すべての例外が先頭の広範囲な例外とマッチしてしまって、常に広範囲の例外のrescueブロックが使用されてしまいます。
begin File.open('sample.txt') do |file| file.each_line do |line| puts line end end rescue SystemCallError => e puts "class=#{e.class},message=#{e.message}" rescue IOError => e puts "class=#{e.class},message=#{e.message}" end
[実行結果]
class=Errno::ENOENT,message=No such file or directory @ rb_sysopen - sample.txt
以上のように、一般的なFile#openメソッドで、ファイルを読み込むだけの際でも、例外が発生する場合も多々あります。
この場合は、「Errno::ENOENT(ファイルが存在していなかった。)」という例外が発生しています。
まとめ
今回は、Rubyの例外処理、rescueについて学習しました!
学習のポイントを振り返ってみましょう!
- begin~endブロックないで起きた例外的なエラーはrescueブロックで処理される。
- rescueブロックでは例外をオブジェクトとして受け取ることができる。
- rescueブロックで受け取る例外は、小さい順に列挙する。
- 例外が発生しても発生しなくても、ensureブロックは実行される。DBコネクションやファイルのクローズ、メモリの解放処理などに使用する。
- rescueブロックから回復した場合、retryでbeginブロックをやり直すことができる。
- raiseメソッドで意図的に例外を発生させることができる。種々の応用が可能。
- 一般的はFile.openメソッドでもrescueで例外を拾える。
今回は、そもそも例外とは何か?というところから、Rubyで例外処理を行う際に使うrescue・raise・ensure・retryメソッドの使い方を説明しました。
デバッグを行う際には必ず必要となる例外処理ですから、例外の基本的理解に加えて、各メソッドの使い方についてはしっかり把握した上で例外処理をマスターしましょう!
もしrescueの使い方について忘れてしまったらこの記事を確認してくださいね!