Learning ruby by the cherry book! (Chapter 9: Exeption)
Ruby Programming
プログラミング言語の再学習としてプロを目指す人のためのRuby入門[改訂2版]を読み始めましたので、気になる点をまとめます。
例外時の出力の読み解き方
# ruby 2.6.3
irb(main):001:0> 1 + '10'
Traceback (most recent call last):
5: from /usr/bin/irb:23:in `<main>'
4: from /usr/bin/irb:23:in `load'
3: from /Library/Ruby/Gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
2: from (irb):1
1: from (irb):1:in `+'
TypeError (String can't be coerced into Integer)
(String can't be coerced into Integer)
例外に関するメッセージ
直訳:文字列を整数に変換することはできません
例外のクラス名
Rubyでは例外クラスのインスタンスになっている
型に関するエラーであることを示している
Traceback (most recent call last):
5: from /usr/bin/irb:23:in <main>'
4: from /usr/bin/irb:23:in
load’
3: from /Library/Ruby/Gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in <top (required)>'
2: from (irb):1
1: from (irb):1:in
+’
1 ~ 5まで呼出履歴となっている
# 例外を補足する
```ruby
puts 'Start.'
begin
1 + '10'
rescue
puts '例外発生したよ'
end
puts 'End.'
# 実行結果
ruby sample/ruby-book/exeption/sample.rb
Start.
例外発生したよ
End.
例外オブジェクトから情報取得
begin
1 / 0
rescue => e
puts "エラークラス: #{e.class}"
puts "エラーメッセージ: #{e.message}"
puts "バックトレース: \n#{e.backtrace.reverse.join("\n")}"
end
# 出力
ruby sample/ruby-book/exeption/sample_error_info.rb
エラークラス: ZeroDivisionError
エラーメッセージ: divided by 0
バックトレース:
sample/ruby-book/exeption/sample_error_info.rb:2:in `<main>'
sample/ruby-book/exeption/sample_error_info.rb:2:in `/'
補足する例外を限定する
begin
1 / 0
rescue ZeroDivisionError
puts "ゼロで除算したエラー"
end
ZeroDivisionError
は補足されて、例外終了しない
ruby sample/ruby-book/exeption/sample_restrict_error.rb
ゼロで除算したエラー
別の例外クラスのエラーを発生させると捕捉されない
begin
# 定義してないメソッド呼び出しエラーを発生させる
'abc'.foo
rescue ZeroDivisionError
puts "ゼロで除算したエラー"
end
ruby sample/ruby-book/exeption/sample_restrict_error_2.rb
Traceback (most recent call last):
sample/ruby-book/exeption/sample_restrict_error_2.rb:3:in `<main>': undefined method `foo' for "abc":String (NoMethodError)
複数のエラーを補足する
例外クラスによって処理を分ける
begin
# 定義してないメソッド呼び出しエラーを発生させる
'abc'.foo
rescue ZeroDivisionError
puts "ゼロで除算したエラーが発生したよ"
rescue NoMethodError
puts "未定義のメソッド呼び出しエラーが発生したよ"
end
同じ処理を呼び出す
begin
# 定義してないメソッド呼び出しエラーを発生させる
'abc'.foo
rescue ZeroDivisionError, NoMethodError
puts "ゼロで除算したか、未定義のメソッド呼び出しエラーが発生したよ"
end
エラー情報を取得する
begin
# 定義してないメソッド呼び出しエラーを発生させる
'abc'.foo
rescue ZeroDivisionError, NoMethodError => e
puts "ゼロで除算したか、未定義のメソッド呼び出しエラーが発生したよ"
puts "エラー: #{e.class}, #{e.message}"
end
例外クラスの継承関係
- StandardError # 通常のプログラムで発生しやすいエラーのスーパークラス
- 特殊なエラー(NoMemoryError, SystemExitなど)
classDiagram Exception <|-- 特殊なエラー Exception <|-- StandardError StandardError <|-- RunTimeError StandardError <|-- NameError NameError <|-- NoMethodError StandardError <|-- TypeError StandardError <|-- ArgumentError StandardError <|-- その他Error
rescue節に何も指定しない場合に補足されるのはStandardErrorとそのサブクラスになる
指定した場合はそのクラスとサブクラスが補足される
begin
# 例外
rescue
# StandardErrorとそのサブクラスのみ補足
end
begin
# 例外
rescue NameError
# NoMethodError はここで補足される
rescue NoMethodError
# ここは永遠に実行されない
end
begin
# 例外
rescue NoMethodError
# これなら実行される
rescue NameError
# NameErrorとそのサブクラスのみ補足
end
retry
たまに書きたくなるので写経しておく
retry_count = 0
begin
puts '処理開始'
1 / 0
rescue
retry_count += 1
if retry_count <= 3
puts "retry! #{retry_count}回目"
retry
else
puts 'retryに失敗'
end
end
raise
例外を意図的に発生させるにはraise
を使う
raise #=> エラーメッセージを省略できるがあまり良くない
raise '例外が発生しました' #=> RuntimeErrorクラスの例外が発生する
raise ArgumentError.new('引数の例外が発生') # 例外クラスの指定
Best practice
安易にrescueを使わない
- 例外が発生した場合、異常終了させて原因を治すべき
- 変に例外をハンドリングしてしまうと内部のデータを壊したりすることもある
- Railsなどではうまくハンドリングする機構がフレームワーク自体の備わっているので委ねるのが良い
rescueしたら情報を残す
安易に例外を処理するべきはないが必要なことも有る
例えば、100人にメールを送るときに、1人目で例外が発生、異常終了しその後99人に送信されないようなことがあったらこまる
そういうときは例外処理をいれ、できる限りの情報を残すこと
users.each do |user|
begin
send_mail_to(user)
rescue =? e
puts e.full_massage
end
end
例外処理は対象範囲と対象クラスを極力絞り込む
# Bad
def convert_reiwa_to_date(reiwa_text)
begin
m = reiwa_text.match(/令和?<jp_year>\d+)年(?<month>\d+)月(?<day>\d+)日/)
year = m[:jp_year].to_i + 2018
month = m[:month].to_i
day = m[:day].to_i
Date.new(year, month, day)
rescue
# 例外の場合 nil を返却
nil
end
end
# Good
def convert_reiwa_to_date(reiwa_text)
# Beginの外に出す=例外処理させる範囲を絞る
m = reiwa_text.match(/令和?<jp_year>\d+)年(?<month>\d+)月(?<day>\d+)日/)
year = m[:jp_year].to_i + 2018
month = m[:month].to_i
day = m[:day].to_i
begin
Date.new(year, month, day)
# rescueする例外クラスも絞る
rescue ArgmentError
# 例外の場合 nil を返却
nil
end
end
例外処理よりも条件分岐を使う
例外処理よりもパフォーマンス面でも優れている
def convert_reiwa_to_date(reiwa_text)
# Beginの外に出す=例外処理させる範囲を絞る
m = reiwa_text.match(/令和?<jp_year>\d+)年(?<month>\d+)月(?<day>\d+)日/)
year = m[:jp_year].to_i + 2018
month = m[:month].to_i
day = m[:day].to_i
if Date.valid_date?(year, month, day)
Date.new(year, month, day)
end
end
予期しない条件は異常終了させる
# Bad
case country
when :japan
'yen'
when :us
'dollar'
else
'rupee'
end
# Good
case country
when :japan
'yen'
when :us
'dollar'
else
raise ArgumentError, "無効な国名です。#{country}"
end
例外処理もテストする
もっと詳しく
ensure/else
encure
: 例外が発生してもしなくても必ず実行させたいときに使うelse
: 例外が発生しなかった場合に実行する
begin
# 例外の可能性がある処理
rescue
# 例外発生時の処理
else
# 例外が発生しない場合の処理
ensure
# 例外有無によらず実行する処理
end
ただし、else
を使わずに以下のように実装することができるので、あまり使わない。
else
を使うとelse
節中でエラーが発生した場合に、手前のrescue
で補足されないという違いが有る。
begin
# 例外の可能性がある処理
# 例外が発生しない場合の処理
rescue
# 例外発生時の処理
ensure
# 例外有無によらず実行する処理
end
begin/endを省略するrescue修飾子
rescue
修飾子を使うと短く記述できる。
StandardErrorとそのサブクラスしか補足できない。(例外クラスの指定ができない)
# 前段の式の評価時の例外を補足
1 / 0 recure 0
# より具体的な例
def to_date(string)
Date.parse(string) rescue nil
end
to_date('2022-09-01')
to_date('hogehoge') #=> nil
$!, $@
$!
: 最後に発生した例外インスタンス
$@
: バックトレース情報
可読性を考えると使わないほうが無難
例外処理 begin/endを省略
メソッド全体やブロック全体が例外処理で囲まれている場合に省略可能
{}
で囲うブロックでは不可
def fizz_buzz(n)
begin
if n % 15 == 0
'Fizz Buzz'
elsif n % 3 == 0
'Fizz'
elsif n % 5 == 0
'Buzz'
else
n.to_s
end
rescue => e
puts "#{e.class} #{e.message}"
end
end
def fizz_buzz(n)
if n % 15 == 0
'Fizz Buzz'
elsif n % 3 == 0
'Fizz'
elsif n % 5 == 0
'Buzz'
else
n.to_s
end
rescue => e
puts "#{e.class} #{e.message}"
end
users.each do |user|
begin
send_mail_to(user)
rescue => e
puts e.full_message
end
end
users.each do |user|
send_mail_to(user)
rescue => e
puts e.full_message
end
補足した例外を再度発生させる
rescue
節内でraise
メソッドを使うことができる。
例外を補足し、異常終了前になにか処理をはさみたいときに使える。
def fizz_buzz(n)
begin
if n % 15 == 0
'Fizz Buzz'
elsif n % 3 == 0
'Fizz'
elsif n % 5 == 0
'Buzz'
else
n.to_s
end
rescue => e
puts "[LOG]エラーが発生しました。 #{e.class} #{e.message}"
raise
end
end
独自の例外クラスを定義する
# 名前だけ変える
class NoCountryError < StandardError
end
# 独自のメソッドや属性を追加する
class NoCountryError < StandardError
attr_reader :country
def initialize(message, country)
@country = country
super("#{message} #{country}")
end
end
def currency_of(country)
case country
when :japan
'yen'
when :us
'dollar'
when :india
'rupee'
else
raise NoCountryError.new('無効な国名です', country)
end
end
begin
currency_of(:italy)
rescue NoCountryError => e
puts "#{e.message} #{e.country}"
end