Techioz Blog

ルビィ「決まった?」オペレータの動作が間違っていますか?

概要

コードは次のようになります。

class Foo
  def bar
    puts "Before existent: #{(defined? some_variable)}"
    puts "Before not_existent: #{(defined? nonexistent_variable)}"

    raise "error"

    some_variable = 42
  rescue
    puts "exception"
  ensure
    puts "Ensure existent: #{(defined? some_variable)}"
    puts "Ensure not_existent: #{(defined? nonexistent_variable)}"
  end
end

そしてそれを irb から呼び出します。

> Foo.new.bar

そして、つまり、次のものが返されます:

Before existent:
Before not_existent:
exception
Ensure existent: local-variable
Ensure not_existent:
=> nil

そして次は疑問です - なぜですか? some_variable を定義する前に例外を発生させました。 なぜこのように機能するのでしょうか? some_variable が ensure ブロックで定義されているのはなぜですか? (ちなみに、nilとして定義されています)

アップデート: @Maxの回答に感謝しますが、インスタンス変数を使用するようにコードを変更すると、次のようになります。

class Foo
  def bar
    puts "Before existent: #{(defined? @some_variable)}"
    puts "Before not_existent: #{(defined? @nonexistent_variable)}"

    raise "error"

    @some_variable = 42
  ensure
    puts "Ensure existent: #{(defined? @some_variable)}"
    puts "Ensure not_existent: #{(defined? @nonexistent_variable)}"
  end
end

期待通りに動作します:

Before existent:
Before not_existent:
Ensure existent:
Ensure not_existent:

なぜ?

解決策

最初に気づくのは、それが定義されているかどうかです。はメソッドではなくキーワードです。つまり、実行時に動的に検索するのではなく、構文ツリーの構築時に (if、return、next などと同様に) 解析中にインタープリターによって認識される独自の特別な VM 命令があることを意味します。

これがなぜ定義されるのでしょうか?パーサーはその引数を通常の評価プロセスから除外できるため、通常エラーが発生する式を処理できます: 定義済み?(これは一体何ですか) #=> nil。

キーワードであっても、その動作は実行時に決定されます。パーサーマジックを使用して、その引数がインスタンス変数、定数、メソッドなどであるかどうかを判断しますが、その後、通常の Ruby メソッドを呼び出して、これらの特定の型が実行時に定義されているかどうかを判断します。

// ...
case DEFINED_GVAR:
if (rb_gvar_defined(rb_global_entry(SYM2ID(obj)))) {
    expr_type = DEFINED_GVAR;
}
break;
case DEFINED_CVAR:
// ...
if (rb_cvar_defined(klass, SYM2ID(obj))) {
    expr_type = DEFINED_CVAR;
}
break;
case DEFINED_CONST:
// ...
if (vm_get_ev_const(th, klass, SYM2ID(obj), 1)) {
    expr_type = DEFINED_CONST;
}
break;
// ...

その rb_cvar_define 関数は、たとえば Module#class_variable_define? によって呼び出される関数と同じです。

それで定義されましたか?変だ。本当に奇妙だ。その動作は引数に応じて大きく異なる可能性があり、異なる Ruby 実装間で同じであるとは思えません。これに基づいて、私はそれを使用せず、代わりに Ruby の *_define? を使用することをお勧めします。可能な限り方法を使用します。