スポンサーリンク

Rubyの動かないコード (中級編) ローカル変数の「暗黙の初期化」に関するエラー(ローカル変数のスコープが事前コンパイルで決まる)

以下のRubyのコードが,意図した動作をしないのはなぜですか。(制限時間1分)

やりたい事:

  • 画面に「1」と表示する。
# aに1を代入する
eval "a = 1"

# aを呼び出す
p a

発生する問題

実行すると下記のエラーになる。

undefined local variable or method `a' for main:Object (NameError)


そんな馬鹿な,と思うだろう。

だって,Rubyはスクリプト言語じゃないか。

コンパイルが不要で,動的に実行されるはず。

1行目が実行された後で,2行目が読み取られてゆくのでは?

1行目で変数が宣言されれば,2行目でその変数を利用できるのでは・・・?


と思うかもしれない。


だが,残念ながら,その認識は少々間違っている。

原因

Rubyは確かに動的で柔軟なスクリプト言語だ。しかし,

Rubyは,最初にスクリプト全体をコンパイルして,ローカル変数を決定する。


以下のURLを参照。

http://www.ruby-lang.org/ja/man/html/...
rubyが最初にスクリプト全体をコンパイルしてローカル変数を決定するから


クラスを動的に宣言したり,ローカル変数の動的な片付けや代入はOKなのだが,

ローカル変数の動的な「宣言」はNG。


派生する問題(1)

このローカル変数の性質によって,派生して起こり得る問題がある。

以下のコードを見てほしい。

if false
  # この中は実行されない

  # 変数を宣言する
  x = 1
  
  # 変数を呼び出す
  p x
else
  # 変数宣言を忘れたまま,変数を呼び出す
  p x
end

p "end successfully"


上のコードではif句内は実行されず,else句内がいきなり実行される。

だから,

「宣言していない変数をいきなり呼び出しているので,例外が発生するのでは?」

と思うだろう。


でも,そうではない。

実行結果:

nil
"end successfully"

if句内は確かに実行されない。


しかし,実行されないにもかかわらず,

「ローカル変数 x を宣言しているぞ!」というのはRubyに悟られて(認識されて)しまうのである。

そして,ローカル変数のデフォルトの値としてnilで初期化されてしまう。


だから,else句内は,(誤って)「正常に」実行されてしまう。



この,「動いてはならないものが,誤って正常に動いているように見えてしまう」という事態は,

とても恐ろしい。


コード内にバグを埋め込んでいるのに,一見ちゃんと動いてるので,コーディングのミスに気付かないままだったりするから。



この点で,下記のようなシチュエーションで問題が起こりうる。

  • チーム内に,コピペ癖のある人がいた。(この時点でプログラマとして終わっているが)
  • その人は,if句内とelse句内に,何も考えず,ほぼ同じコードをコピペした。
  • if句とelse句のうち,片方でしか宣言+初期化していないローカル変数があった。
  • もう片方では,その変数は初期化されないまま,「暗黙のnil」で初期化されて,コードが動いていた。
  • でも,エラーにならないので,変数の宣言漏れに気づかない。
  • そもそも,コピペする人間は,「自分がコピペしたコード中のある1つの変数が実際に使われているか」など考えない
  • if内とelse内が肥大化し,リファクタリングの時期がやってきた。
  • if内とelse内をそれぞれメソッドに切り分ける。
  • その時初めて問題が表面化する。nilで暗黙の初期化が行なわれなくなるので, undefined local variable or methodになる。
  • 今まで,どの変数がどう初期化されて動いていたのか,その時点でようやく考え始めることになる。


対策は簡単だ。

  • コピペするな(コピペする人間をチーム内に入れるな)
  • if内とelse内の保持変数が,「一目で把握できないほど複雑化」する前に,それぞれリファクタリングしてメソッド化すること
    • (メソッドの行数を平気で長くする人間をチームに入れるな)

派生する問題(2)

ローカル変数のスコープ絡みで,似たような問題として,以下の点も覚えておこう。

require と load のスコープ
http://doc.okkez.net/static/192/metho...

  • ローカル変数はファイル間では共有されません。ですので、ロードしたライブラリのローカル変数をロード元のスクリプトから直接取得することはできません。このスコープの扱い方はKernel.#loadでも同様です。


サンプルコード:

a.rb

x = 1


b.rb

require "a"

p x
  # => undefined local variable or method `x'


なお,xではなく@xならばOK(ローカル変数ではなくインスタンス変数なので)。