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(ローカル変数ではなくインスタンス変数なので)。