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

Rubyでrescueブロックの外から直近の例外を取得する方法 ― $!とPRYの活用

コードがそのライフサイクルを直接制御していない場合でも、直近に発生した例外を取得できると便利な場面は少なくありません。たとえば、アプリケーションに基本的なクラッシュ検出機能を追加したいとしましょう。捕捉されなかった例外によってクラッシュが起きた際に、詳細な情報をログへ記録したいはずです。

まずは、アプリケーションが終了するたびに実行されるハンドラーを追加することから始めます。これには、Ruby組み込みのat_exitメソッドを使うのが最も手軽です。

at_exit do
  puts "the app exited"
end

しかし、この終了時コールバックが例外をきっかけに呼び出されたものかどうかは、どうすれば判別できるのでしょうか?実はRubyには、$!という一見謎めいた名前のグローバル変数が用意されています。この変数には、現在のコールスタック上のどこかで発生した「直近の例外」が格納されます。

$!を使えば、プログラムが例外によって終了しようとしているのかどうかも簡単に検出できます。具体的には次のように書きます。

at_exit do
  save_error_to_log($!) if $!
end

$!の限界

残念ながら、$!が値を持つのは、例外が現在のコールスタック内で発生した場合に限られます。例外をrescueした後、rescue節の外で$!を参照すると、nilが返されてしまうのです。

begin
  raise "x"
rescue
  puts $! # => RuntimeError
end

puts $! # => nil

この仕様のため、IRBのようなシェル環境では$!はほとんど役に立ちません。IRBでメソッドを実行して例外が発生したとき、その例外オブジェクトを後から取得したくなることがありますが、$!ではそれができないのです。

irb(main):001:0> 1/0
ZeroDivisionError: divided by 0
    from (irb):1:in `/'
irb(main):002:0> $!
=> nil

PRYで$!の限界を回避する

PRYは、独自のローカル変数_ex_を用意することで、$!の限界を見事に回避しています。この変数には、直近の未捕捉例外(uncaught exception)が格納されます。

[1] pry(main)> raise "hi"
RuntimeError: hi
from (pry):1:in `__pry__'
[2] pry(main)> _ex_
=> #<RuntimeError: hi>

PRYがこれを実現できるのは、PRYやIRBの中には実際には「未捕捉」の例外が存在しないからです。シェル自身が例外を捕捉し、見やすく整形されたエラーメッセージとして表示しているのです。

以下は、PRYのソースコードから関連する箇所を抜粋したものです。ユーザーのコマンドを評価するコードが、begin/rescue/endブロックで囲まれているのがわかります。捕捉可能な例外が発生すると、PRYはその例外をself.last_exceptionに保存し、後ほど_ex_へ代入します。

# PRYソースからの抜粋
# https://github.com/pry/pry/blob/623306966bfa86890ac182bc8375ec9699abe90d/lib/pry/pry_instance.rb#L273

begin
  if !process_command_safely(line)
    @eval_string << "#{line.chomp}\n" if !line.empty? || !@eval_string.empty?
  end
rescue RescuableException => e
  self.last_exception = e
  result = e

  Pry.critical_section do
    show_result(result)
  end
  return
end

Englishライブラリを読み込む

$!のような記号だらけの変数名は、少し目に優しくないと感じるかもしれません。幸いなことに、Rubyには「English」というモジュールが標準で含まれており、ロボットの悪口のように見える多くのグローバル変数に、英語名の別名を与えてくれます。

$!の同義語は$ERROR_INFOです。$!を使える場所なら、どこでも代わりに使用できます。

require "English"

begin
  raise "x"
rescue
  puts $ERROR_INFO # => RuntimeError
end

その他の英語名の別名のほとんどは本題とは直接関係ありませんが、参考までに一覧をまとめました。左側がEnglish版の変数名、右側が元の変数名です。

$ERROR_INFO$!
$ERROR_POSITION$@
$FS$;
$FIELD_SEPARATOR$;
$OFS$,
$OUTPUT_FIELD_SEPARATOR$,
$RS$/
$INPUT_RECORD_SEPARATOR$/
$ORS$\
$OUTPUT_RECORD_SEPARATOR$\
$INPUT_LINE_NUMBER$.
$NR$.
$LAST_READ_LINE$_
$DEFAULT_OUTPUT$>
$DEFAULT_INPUT$<
$PID$$
$PROCESS_ID$$
$CHILD_STATUS$?
$LAST_MATCH_INFO$~
$IGNORECASE$=
$ARGV$*
$MATCH$&
$PREMATCH$`
$POSTMATCH$'
$LAST_PAREN_MATCH$+
  1. Rubyのmapメソッド完全ガイド!配列・ハッシュのデータ変換を実例で解説

    Rubyのmapメソッドは、配列(Array)・ハッシュ(Hash)・範囲(Range)といったコレクションに対して使用できる強力なメソッドです。 mapの主な用途は、データの変換です。 例えば、文字列の配列があった場合、すべての文字列を順番に処理して、各文字を大文字に変換することができます。 また、Userオブジェクトのリストがある場合も同様です。 それらを変換して、対応するメールアドレスや電話番号など、Userクラスに定義された任意の属性のリストを作成できます。 それでは、具体的な使い方を見ていきましょう! Ruby mapメソッドの基本構文 mapの構文は次のようになっています。 a

  2. RubyでNクイーン問題を解く方法【バックトラッキングの実装を徹底解説】

    N-Queens(Nクイーン)は、N×Nの盤面にN個のクイーンを配置する、非常に興味深いコーディングチャレンジです。 イメージは以下のようになります。 クイーンは全方向に移動できる駒です。 縦方向 横方向 斜め方向 この問題の解答(複数存在する場合もあります)では、すべてのクイーンを盤上に配置しつつ、どのクイーンも他のクイーンの攻撃範囲に入らないようにする必要があります。 この記事では、私がどのような思考プロセスで解答にたどり着いたのかを紹介します。 まずは計画を立てる この種のチャレンジに取り組むときは、まず平易な言葉で計画を書き出すことから始めるのが効果的です。 そうすることで、「何