クラス変数の参照の仕組みって難しい

去年の2月にRuby技術者認定試験Goldを受けたのですが、受験時は実は理解が微妙で、ある程度時間が経過してから少しずつ理解がついてくるということがちょくちょくあって面白いなと感じている今日この頃。

Ruby Goldの試験対策をしていた当時、どうしても分からなかったのがクラス変数の参照の仕組みでした。

class C
  @@val = 1
end

class C1 < C
end

p C.class_variables
p C1.class_variables

このコードの場合は

$ ruby test.rb
[:@@val]
[:@@val]

クラス変数を参照できます。

続いて別のパターンを見ていきます。

# test.rb

module M
  @@val = 10

  class C
    p @@val
  end
end

このコードを実行するとどうなるかというと

$ ruby test.rb
# => test.rb:5:in `<class:C>': uninitialized class variable @@val in M::C (NameError)

となります。 これはクラス C では @@val は定義されていないのでエラーになるというのは納得です。

では、以下の場合はどうかというと

# test.rb

class C
  @@val = 20
end

module B
  @@val = 30
end

module M
  include B
  @@val = 10

  class << C
    p @@val
  end
end
$ ruby test.rb
10

10 が返ってきました。

この例を見た時、なんで 10 が返ってくるのか理解できませんでした。 資格試験の学習に取り組んでいた当時の私は最初このコードは例外が発生すると思っていて、クラス変数が何れかの値を参照するという点がそもそも分からなかったという感じです。

続けて、少し変えたコードで試してみようと思います。

# test.rb

module M
  @@val = 10

  class C
  end

  class << C
    p @@val
  end
end

これも例外が発生しそうな気がしますが、実行してみると

$ ruby test.rb
10

10 が返ってきました!

もう1パターン試してみます。 これは1つ前の例を書き換えたものです。

# test.rb

module M
  @@val = 10

  class C
    class << self
      p @@val
    end
  end
end

実行してみると

$ ruby test.rb
test.rb:6:in `singleton class': uninitialized class variable @@val in M::C (NameError)

例外が発生! なんでーーー?

ここまで幾つかの例でクラス変数の参照がどうなるかを見てきましたが、結果に納得がいったのは最初の例2つだけで、残りはなんでそうなるのかが説明できない状態でした。

ちなみにRubyリファレンスマニュアルには、

クラス変数は、その場所を囲むもっとも内側の(特異クラスでない) class 式または module 式のボディをスコープとして持ちます。

と書いてあり、特異クラスを理解する必要があることがわかりました。

Rubyのしくみを読んだらちょっとずつ分かってきた

上で見てきたクラス変数の参照の結果がなぜそうなるのかについてRubyのしくみを読んだことでようやく自分の中で整理がついたため、備忘録も兼ねてまとめておきたいと思います。

Rubyのしくみの第9章:メタプログラミングを読んでいると「レキシカルスコープ内の特異クラスを使ってメソッドを定義する」という解説をしている部分があり、

class <<構文を使って新しいレキシカルスコープを宣言することもできる。

と書いてありました。

レキシカルスコープについては第6章:メソッド探索と定数探索の中に解説があり、

レキシカルスコープは、クラス階層でも他のいずれのスキームでもなく、プログラムの構文的な構造上のコードの区分を指す。

とあります。 現在のクラスまたはモジュールに対するスコープとそのコードを取り囲む親の部分をレキシカルスコープとするという理解です。

私が理解できていないと書いたコードに戻ってみたいと思います。

まず、以下の例では class << 構文は module M の中に書かれています。

# test.rb

module M
  @@val = 10

  class C
  end

  class << C
    p @@val
  end
end

p @@val が実行されている 現在のスコープ(self のクラス)は M::C の特異クラスであり、レキシカルスコープは module M ということになります。

そのため、M::C の特異クラスで実行される p @@v はレキシカルスコープを辿って モジュールMで定義されているクラス変数を参照することができると説明がつけられそうです。

続いて、例外が発生する以下の例です。

# test.rb

module M
  @@val = 10

  class C
    class << self
      p @@val
    end
  end
end

この場合、class << selfclass C の中で定義されています。

p @@val が実行されている 現在のスコープは M::C の特異クラスであり、レキシカルスコープは class C の中です。 そのため、module M にある @@val はこのスコープからは参照できず例外が発生します。

# test.rb

class C
  @@val = 20
end

module B
  @@val = 30
end

module M
  include B
  @@val = 10

  class << C
    p @@val
  end
end

この例についてもなぜ 10 が得られるのかがレキシカルスコープの点で見直すと納得がいきました。

ふりかえり

自分が以前理解できなかったことがわかった瞬間はとても嬉しくちゃんとアウトプットしておきたい思い、こちらのブログを書いてみました。

普段のコードでクラス変数を使うことはまずないし、クラス変数よりもクラスインスタンス変数を使った方がいいというのもあって、自分が長らく疑問を感じていたクラス変数の参照の仕組みが何かに役に立つことは多分ないだろうと思いつつ、今回Rubyの特異クラスやレキシカルスコープへの理解を深めるきっかけになったので良かったのかなと思っています。

説明や使っている用語が適切なのかがちょっと心配な部分もあり、間違っている箇所や見直した方が良い部分があればぜひ教えていただけると嬉しいです。