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_evalもdefine_methodも、モジュール経由のミックスインでさえ、ブロックやモジュール本体は元の場所の字句スコープを保持するため、期待どおりには機能しません。
これで、「Rubyではクラス変数を使うな」と誰もが口を揃えて言う理由が、おわかりいただけたのではないでしょうか。:)
-
Rubyで環境変数を使う方法|ENVオブジェクトの操作から設定・Rails credentialsまで徹底解説
環境変数とは、次のような「キーと値」のペアとして表されるデータのことです。 KEY=VALUE この変数は、パソコン上で動くすべてのプログラム間で設定情報(コンフィグ)を共有するために使われています。 だからこそ、環境変数がどのような仕組みで動作し、Rubyプログラムから特殊な ENV オブジェクトを使ってどうアクセスするのかを学んでおくことが大切なのです。 環境変数の活用例 デフォルトエディターの設定 Rubyにgemの保存場所を伝える(GEM_PATH / GEM_HOME) APIキーをGitリポジトリにコミットすることなくアプリケーションへ渡す OSがバイナリファイル(Windo
-
【Ruby】デコレータデザインパターンでクラスを変更せずにオブジェクトを拡張する方法
デコレータ(Decorator)デザインパターンとは、どのようなものなのでしょうか? また、このパターンをRubyのプロジェクトではどのように活用すればよいのでしょうか? デコレータデザインパターンを利用すると、クラスそのものを変更することなく、オブジェクトに新しい機能を追加して拡張することができます。 それでは、具体的な例を見ていきましょう! ログ出力とパフォーマンス計測の実装例 この例では、rest-clientのようなGemを使ってHTTPリクエストを送信します。 コードは次のようになります。 require restclient data = RestClient.get(www.