Ruby初級者向けレッスン 45回 ─例外─

第57回 Ruby/Rails勉強会@関西での初級者向けレッスンの資料を公開します。*1


エラーメッセージ

require 'open-uri'
open 'http://github.com/rubykansai/workshops/wiki'
.../open-uri.rb:223:in `open_loop': redirection forbidden: http://github.com/rubykansai/workshops/wiki -> https://github.com/rubykansai/workshops/wiki (RuntimeError)
        from .../open-uri.rb:149:in `open_uri'
        from .../open-uri.rb:688:in `open'
        from .../open-uri.rb:34:in `open'
        from ex.rb:2:in `<main>'


  • 上 2行が Ruby のコード。
  • 実行すると、下のエラーメッセージが出た。
  • Ruby のエラーメッセージは、いんちき英語でも読める。
  • 「...(RuntimeError)」までが 1行
  • 後は「from ...」から行末までが 1行
  • 最初の行がエラーの出た場所。
    • 「in `open_loop'」は open_loop メソッドでエラーが出たという意味。
    • 「.../open-uri.rb:223」は open-uri.rb の 223行目でエラーが出たという意味。
      • えっ、でも open_loop なんて見覚えないよ!?
  • 最後の行が呼び出し元。
    • 「in `<main>'」は実行を開始したオブジェクト。
  • 下から 2行目「in `open'」は <main> から open メソッドが呼ばれたという意味。
    • 各行がメソッドの呼び出しを表している。*2
  • 結局、英語のメッセージを読む必要がある ><
    • 「redirection forbidden」理由は分からんが http から https へのリダイレクトが禁止されてるらしい。
  • 最初の行の行末「(RuntimeError)」は RuntimeError 例外が発生したという意味。

エラーメッセージと例外


  • 例外が起きたら、なんらかの対処をしないと、プログラムが異常終了する。

いろいろな例外と、そのエラーメッセージを見て、例外に慣れよう。

例外いろいろ TypeError

"1" + 1

# ~> ex.rb:1:in `+': can't convert Fixnum into String (TypeError)
# ~> 	from ex.rb:1:in `<main>'


  • 下のコメントは実行した際のエラーメッセージ*3
  • String の "1" に Fixnum の 1 は足せない。

例外いろいろ NoMethodError

1.to_sym

# ~> ex.rb:1:in `<main>': undefined method `to_sym' for 1:Fixnum (NoMethodError)

  • Fixnum の 1 に to_sym というメソッドはない。

例外いろいろ NameError

n.times{puts 'Ruby!'}

# ~> ex.rb:1:in `<main>': undefined local variable or method `n' for main:Object (NameError)

  • n という変数やメソッドはない。

例外いろいろ NoMethodError (再び)

n = ARGV.first.to_i unless ARGV.empty?
n.times{puts 'Ruby!'}

# ~> ex.rb:2:in `<main>': undefined method `times' for nil:NilClass (NoMethodError)

  • コマンドライン引数を数値に変換して n に代入する。
  • 変数 n は nil になる。
    • nil は NilClass のインスタンスで、他の言語でいう NULL のこと。
    • nil はオブジェクトなのでメソッド呼び出しもできる。
    • しかし nil には times メソッドはない。

例外いろいろ Errno::ENOENT

open('nothing.txt')

# ~> ex.rb:1:in `initialize': No such file or directory - nothing.txt (Errno::ENOENT)
# ~> 	from ex.rb:1:in `open'
# ~> 	from ex.rb:1:in `<main>'

  • 存在しないファイルを open しようとした。
  • 「in `initialize'」は File.open メソッドが Fileクラスのインスタンスを生成しようとしている。
  • Errno::Exxx の例外は OS の errno に対応した例外。

例外いろいろ SyntaxError

1 def even?(n)
2   if n % 2 == 0
3     true
4   else
5     false
6 end
7 
8 puts even?(0)
9 puts even?(1)

# ~> -:9: syntax error, unexpected end-of-input, expecting keyword_end

  • if-else-end の end を忘れている。
  • Ruby のパーサがスクリプトを最後まで読んだが、end が見つからなかった。
  • end を忘れたのは 5行目と 6行目の間だが、例外は最後で出るので注意。

Intelligence と Wisdom

雨が降ってきて…
  • Intelligence
    • 雨だ!
  • Wisdom
    • 傘を差そう
    • 雨宿りしよう


雨が降ってきたときに、

  • それが雨だと認識できる ─── Intelligence (知識)
  • 濡れないためには、傘を差すか、雨宿りしないと ─── Wisdom (知恵)

例外も同じで、

  • エラーを検知したものが例外を起こす (Intelligence)
  • 対処方法を知っているものが例外を捕捉する (Wisdom)

例えば、アプリケーションがファイルを読もうとして、

  • open できない!
  • read できない!

File ライブラリはエラーを検知できる。(Intelligence)

  • 既定値を使うから、読めなくてもよい。
  • 別のファイルから読みなおしたい。
  • etc...

これはアプリケーションの仕様。(Wisdom)
ライブラリには分かりようがない。

例外を捕捉する (コード例1)

files = %w[file.txt file1.txt file2.txt]

files.each do |fn|
  begin
    open(fn, 'w'){|f| f.puts "Ruby!!"}
    break
  rescue => ex
    $stderr.puts "#{ex} (#{ex.class})"
  end
end


やりたいこと

  • ファイルに "Ruby!!" と書き込みたい。
  • ファイル名は file.txt
    • もし書けなければ、代わりに file1.txt に書こう。
    • それでも書けなければ、file2.txt に書こう。
    • それでダメなら諦める。

コード解説

  • ファイル名 "file.txt", "file1.txt", "file2.txt" という Array を用意。
  • 各ファイル名に対して処理を繰り返す。─── files.each ...
    • begin - end で例外を捕捉する準備。
    • ファイルに "Ruby!!" を書き込む。─── open( ...
      • 例外が起きると、rescue節に処理が移る。
      • 変数 ex が例外オブジェクトを指す。
      • ここでは、標準エラー出力に例外情報を出力するだけ。
      • 最初の繰り返しに戻る。─── files.each ...
    • 次のファイル名で open & puts に再挑戦。
      • 例外が起きなければ break で繰り返しを脱出する。

やってみる

$ ls -Fdl file*
drwxr-xr-x  2 mas  staff  68  4 12 23:37 file.txt/
-r--r--r--  1 mas  staff   0  4 12 23:37 file1.txt
# file.txt はディレクトリ
# file1.txt は書き込み権なし

$ ruby retry.rb
Is a directory - file.txt (Errno::EISDIR)
Permission denied - file1.txt (Errno::EACCES)

$ ls -Fdl file*
drwxr-xr-x  2 mas  staff  68  4 12 23:37 file.txt/
-r--r--r--  1 mas  staff   0  4 12 23:37 file1.txt
-rw-r--r--  1 mas  staff   7  4 12 23:39 file2.txt
# file2.txt ができた!

$ cat file2.txt 
Ruby!!

例外を捕捉する

begin
  式1…
[rescue [型1[, 型2]…][=> 変数][then]
  式2…]…
[else
  式3…]
[ensure
  式4…]
end

  • [ ... ] 内は省略可能。
  • begin - end で例外を捕捉する準備。
  • 式1 が例外を捕捉したい処理 (複数書ける)
  • rescue節で例外を捕捉。(rescue は複数書ける)
    • 「型」に例外クラスを指定すると、特定の例外だけを捕捉できる。(型は複数書ける)
    • 式2 が例外対処のための処理 (複数書ける)
    • 発生した例外に対応する rescue節がなければ、捕捉できない。
  • ensure節は begin - end のブロックを抜ける際の後始末。
    • 例外が起きても、起きなくても実行される。
    • 式4 が後始末の処理 (複数書ける)
  • else節は、上記 式1 で例外が起きなかった際に実行される。
    • rescue節で指定しなかった例外を捕捉する処理ではない。
    • 式3 で起きた例外は、このブロックでは捕捉しない。

上記のコードを省略なしで書くと、3通りの実行パターンがある。

  • 式1→式3→式4 (例外なし)
  • 式1→式2→式4 (例外を捕捉)
  • 式1→式4 (例外が発生するが、捕捉できない)

例外を捕捉する (コード例2)

require './factorial'

def fact(n)
  n.factorial
rescue ArgumentError
  "1以上の整数を指定してください"
rescue NoMethodError
  "整数を指定してください"
end

fact 3      # => 6
fact 4      # => 24
fact 5      # => 120
fact 0      # => "1以上の整数を指定してください"
fact 2.5    # => "整数を指定してください"
fact "2"    # => "整数を指定してください"

  • メソッド定義の def - end にも rescue, else, ensure が書ける。*4

例外の種類

puts NoMethodError.ancestors
# >> NoMethodError
# >> NameError
# >> StandardError
# >> Exception
# >> Object
# >> Kernel
# >> BasicObject

  • NoMethodError は NameError のサブクラス
  • NameError は StandardError のサブクラス
  • StandardError は Exception のサブクラス
  • Exception は例外の基底クラス

rescue修飾子

require './factorial'

3.factorial   rescue 0  # => 6
4.factorial   rescue 0  # => 24
5.factorial   rescue 0  # => 120
0.factorial   rescue 0  # => 0
2.5.factorial rescue 0  # => 0
"2".factorial rescue 0  # => 0

  • rescue の後置。
    • まず rescue の左辺を評価。
    • 例外が起きると rescue の右辺を評価。
  • 例外が起きたら、結果は 0 でいい。という大雑把なコード。
  • 捕捉する例外クラスを指定できない。
  • 例外オブジェクトを指す変数を指定できない。

例外を起こす

raise "simple"

# ~> ex.rb:1:in `<main>': simple (RuntimeError)

  • 例外を起こすには Kernel#raise メソッドを使う。
  • RuntimeError が発生する。

例外を起こす (クラス指定)

raise ArgumentError, "bad argument"

# ~> ex.rb:1:in `<main>': bad argument (ArgumentError)

  • 例外クラスを指定して raise できる。

例外を起こす (オブジェクト)

raise TypeError.new("can't convert...")

# ~> ex.rb:1:in `<main>': can't convert... (TypeError)

  • 例外オブジェクトを raise することができる。*5

例外を起こす (独自の例外クラス)

class MyError < StandardError; end

raise MyError, 'original'

# ~> ex.rb:3:in `<main>': original (MyError)

  • 独自の例外クラスを作れる。*6
    • 普通は StandardError のサブクラスにする。*7

サンプルコード

スライド、コードに間違いがあれば、ご指摘ください。

*1:スライドだけ欲しい人は直接どうぞ http://higaki-it.jp/ruby/57/print.pdf

*2:スタックトレースとかバックトレースとか言う。

*3:あれ? 今実行したらメッセージ違う!?

*4:else節の評価結果はメソッドの評価結果となるが、ensure節の評価結果はメソッドの評価結果とはならない。

*5:例外でないオブジェクトを指定すると TypeError が発生する。えっ?

*6:エンタープライズだと独自の例外じゃないと起こせないことあるよね。

*7:StandardError なら、クラス指定なしの rescue でも捕捉できるから。