Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyクラス変数の罠――字句スコープが引き起こす予期せぬ挙動

Rubyのクラス変数(@@variable)は紛らわしいことで有名です。熟練したRuby開発者であっても、その挙動を直感的に理解するのは容易ではありません。最もわかりやすい例が、継承に関するものです。

class Fruit
  @@kind = nil

  def self.kind
    @@kind
  end
end

class Apple < Fruit
  @@kind = "apple"
end

Apple.kind
# => "apple" 

Fruit.kind
# => "apple" 

子クラスでkindを変更すると、なんと親クラスの値まで変わってしまうのです。かなり厄介な仕様ですが、実はこれは言語として意図された動作であり、かつてSmalltalkを模倣するために下された設計上の決定によるものです。

さらに厄介な話

クラス変数には他にも奇妙な挙動があり、それらはアーキテクチャ上の意図的な選択というより、むしろ実装上の癖と言えるものです。今回はその中でも特に興味深いと感じる例を取り上げ、少し掘り下げてみたいと思います。

まず、2つのコード断片を比較してみましょう。一見すると同じ結果を返すように見えますが、実際にはそうなりません。

最初の例では、クラス変数を設定し、それを返すメソッドを作成しています。特別なことは何もしていませんが、すべて期待どおりに動作します。

class Foo
  @@val = 1234

  # クラスメソッドを宣言するショートハンド
  class << self
    def val
      @@val
    end
  end
end

Foo.val
# => 1234

ご存じなかったかもしれませんが、class << selfは必ずしもクラス定義の内側に置く必要はありません。次の例では、これをクラス定義の外側へ移動しています。クラスメソッド自体は追加されますが、今度はクラス変数にアクセスできないのです。

class Bar
  @@val = 1234
end

class << Bar
  def val
    @@val
  end
end

Bar.val

# warning: class variable access from toplevel
# NameError: uninitialized class variable @@val in Object

メソッドからクラス変数へアクセスしようとすると、警告と例外が発生します。一体何が起きているのでしょうか?

ここで登場するのが字句スコープ

Rubyの奇妙で紛らわしい挙動の99%は、字句スコープ(レキシカルスコープ)に起因していると、筆者はますます確信するようになりました。

用語に馴染みがない方のために簡単に説明すると、字句スコープとは、抽象的なオブジェクトモデル上での所属ではなく、コード内での物理的な記述位置に基づいて要素をグループ化することを指します。言葉で説明するより、例を見る方が早いでしょう。

class B
  # x と y は同じ字句スコープを共有する
  x = 1
  y = 1
end

class B
  # z は同じクラス内にあるにもかかわらず、x・y とは異なる字句スコープを持つ
  z = 3
end

クラス変数の所属クラスは字句的に決定される

では、先ほどのクラス変数の例では、字句スコープがどのように作用しているのでしょうか?

クラス変数を取得する際、Rubyは「どのクラスから」その値を取り出すべきかを知る必要があります。そして、このクラスの特定に字句スコープが使われているのです。

動作する例をよく観察すると、クラス変数へアクセスするコードが、物理的にもクラス定義の内側に置かれていることがわかります。

class Foo  
  class << self
    def val
      # 私はFooに字句スコープされている!
      @@val
    end
  end
end

一方、動作しなかった例では、クラス変数へアクセスするコードはクラスに字句スコープされていません。

class << Bar
  def val 
    # ここからFooは見えない
    @@val
  end
end

では、クラスに字句スコープされていないなら、どこにスコープされているのでしょうか? ヒントはRubyが出力する警告文にあります:warning: class variable access from toplevel

実は、動作しない例ではクラス変数はトップレベルオブジェクトに字句スコープされていたのです。これにより、非常に奇妙な振る舞いが発生しかねません。

例えば、mainに字句スコープされたコードからクラス変数を設定しようとすると、そのクラス変数はBarではなくObjectに設定されます。

class Bar
end

class << Bar
  def val=(n)
    # このコードはトップレベルオブジェクトに字句スコープされている。
    # つまり @@val は Bar ではなく Object に設定される!
    @@val = n
  end
end

Bar.val = 100

# えっ?
Object.class_variables
# => [:@@val]

その他の失敗例

クラスの字句スコープ外からクラス変数を参照する方法は実にさまざまありますが、どの方法を選んでもトラブルの種になります。

いくつか例を挙げてみましょう。

class Foo
  @@foo = :foo
end

# これは動かない
Foo.class_eval { puts @@foo }

# これもダメ
Foo.send :define_method, :x do 
  puts @@foo
end

# まさかモジュールを使おうとは思っていないよね?
module Printable
  def foo
    puts @@foo
  end
end

class Foo
  @@foo = :foo
  include Printable
end

Foo.new.foo

class_evaldefine_methodも、モジュール経由のミックスインでさえ、ブロックやモジュール本体は元の場所の字句スコープを保持するため、期待どおりには機能しません。

これで、「Rubyではクラス変数を使うな」と誰もが口を揃えて言う理由が、おわかりいただけたのではないでしょうか。:)

  1. Rubyで環境変数を使う方法|ENVオブジェクトの操作から設定・Rails credentialsまで徹底解説

    環境変数とは、次のような「キーと値」のペアとして表されるデータのことです。 KEY=VALUE この変数は、パソコン上で動くすべてのプログラム間で設定情報(コンフィグ)を共有するために使われています。 だからこそ、環境変数がどのような仕組みで動作し、Rubyプログラムから特殊な ENV オブジェクトを使ってどうアクセスするのかを学んでおくことが大切なのです。 環境変数の活用例 デフォルトエディターの設定 Rubyにgemの保存場所を伝える(GEM_PATH / GEM_HOME) APIキーをGitリポジトリにコミットすることなくアプリケーションへ渡す OSがバイナリファイル(Windo

  2. 【Ruby】デコレータデザインパターンでクラスを変更せずにオブジェクトを拡張する方法

    デコレータ(Decorator)デザインパターンとは、どのようなものなのでしょうか? また、このパターンをRubyのプロジェクトではどのように活用すればよいのでしょうか? デコレータデザインパターンを利用すると、クラスそのものを変更することなく、オブジェクトに新しい機能を追加して拡張することができます。 それでは、具体的な例を見ていきましょう! ログ出力とパフォーマンス計測の実装例 この例では、rest-clientのようなGemを使ってHTTPリクエストを送信します。 コードは次のようになります。 require restclient data = RestClient.get(www.