【一人 Ruby Advent Calendar 2017】トップレベルの変数の扱い【19日目】

一人 Ruby Advent Calendar 2017 19日目の記事になります。
前回、トップレベルのメソッドの扱いについて書いたのですが、今回は変数に関してです。

トップレベルのローカル変数

トップレベルで定義されたローカル変数は通常のローカル変数と同じような感じで使えます。

homu = "homu"

def func
    homu
end

# 関数内からは参照できない
# Error: undefined local variable or method `homu' for main:Object (NameError)
# p func

# 関数内で参照する場合はブロックを使ってキャプチャする
define_method(:func){
    homu
}
p func
# => "homu"

また、ローカル変数の寿命は『プログラム終了』までで、他のファイルからは参照できません。

トップレベルのインスタンス変数

さて、ややこしいのはトップレベルで定義されているインスタンス変数です。
トップレベルで定義されたインスタンス変数は self、つまり main オブジェクトのインスタンス変数として定義されます。

@homu = "homu"

p self.instance_variables
# => [:@homu]

p self.instance_variable_get :homu
# => "homu"

また、トップレベルのメソッド内等からも参照できるように『見えます』。

@homu = "homu"

def homu
    @homu
end

p homu
# => "homu"

これがちょっと勘違いするポイントなんですが、

わけではなくて

  • トップレベルで呼び出したメソッドのコンテキストが main オブジェクトになる

のでメソッド内で参照しているインスタンス変数が self、つまり main オブジェクトから参照する事になります。
まあ文章で説明してもわかりづらいと思うので実際にコードを書いてみましょう。

def homu
    self
end

# トップレベルで呼び出した場合、homu メソッドのコンテキストは main になる
p homu
# => main

class X
    def func
        homu
    end
end

# 別のオブジェクトのメソッド内で呼び出した場合は、そのオブジェクトのコンテキストになる
p X.new.func
# => #<X:0x00000000008796a0>

このようにトップレベルで定義されたメソッドは『呼びだされたレシーバのコンテキスト』として呼ばれます。
なので、トップレベルのメソッド内でトップレベルのインスタンス変数を参照しようとすると呼び出された場所によって結果が異なります。

@homu = "homu"

def homu
    @homu
end

# トップレベルで呼びだされた場合、コンテキストは main になるので
# main のインスタンス変数、つまりトップレベルの @homu が参照される
p homu
# => "homu"

class X
    def func
        homu
    end
end

# X のインスタンスメソッドから呼び出した場合は、コンテキストが X オブジェクトになるので
# X オブジェクトの @homu を呼びだそうとする
# X オブジェクトのインスタンス変数 @homu は未定義なので nil を返す
p X.new.func
# => nil

うーん、ややこしいですね…。

まとめ

  • トップレベルで定義したローカル変数はそのファイルのローカル変数として定義される
  • トップレベルで定義したインスタンス変数は main オブジェクトのインスタンス変数として定義される
  • トップレベルで定義されたメソッドのコンテキストはレシーバによって異なるので注意する


この挙動は Ruby

  • self がどのように参照されているのか
  • Object で定義されたメソッドはどのように扱われるのか

などを理解していればとても理にかなっている挙動にはなるんですが、そのあたりがわかっていないとかなり難しいところではありますね…。