ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【Ruby v2.6】クラス変数 その2(メソッドで生成した場合)

前置き

クラス変数についてまとめます。 その2は、クラス変数をメソッド内で生成した場合の挙動について。

クラス変数のメソッド内生成

その1でも書きましたが、クラス変数はメソッド外だけでなく、クラスメソッドやインスタンスメソッド内でも生成することができます。

一瞬、便利な感じがしますが、「継承」が絡んでくると、クラス変数がどのクラスで生成されるのか、とても分かり辛くなるので要注意です。

以下のようなクラスを作ってみます。

class A
    def self.class_method_a1
        @@a1 = "created by class method a1 in A."
    end

    def self.class_method_a2
        @@a2 = "created by class method a2 in A."
    end

    def self.class_method_a3
        @@a3 = "created by class method a3 in A."
    end
end

class B < A
    def self.class_method_a2
        @@a2 = "created by class method a2 in B."
    end
end

class C < B
    def self.class_method_a3
        @@a3 = "created by class method a3 in C."
    end
end

クラス A では、class_method_a1class_method_a2class_method_a3 の3つのクラスメソッドを定義し、それぞれでクラス変数を生成しています。

クラス B では、クラス A を継承し、class_method_a2 だけをオーバーライドして、オーバーライド前と同じクラス変数 @@a2 を生成しています。

クラス C では、クラス B を継承し、こちらは class_method_a3 をオーバーライドして、オーバーライド前と同じクラス変数 @@a3 を生成しています。

この状態で、クラス A のクラスメソッド3つを呼び出してみます。

A.class_method_a1        # => "created by class method a1 in A."
A.class_method_a2        # => "created by class method a2 in A."
A.class_method_a3        # => "created by class method a3 in A."
A.class_variables        # => [:@@a1, :@@a2, :@@a3]
B.class_variables        # => [:@@a2, :@@a1, :@@a3]
C.class_variables        # => [:@@a2, :@@a1, :@@a3]

クラス A で @@a1@@a2@@a3 が生成され、継承しているクラス B、クラス C でも認識されています。

・・・しかし・・・

一度リセットして、クラス A のクラスメソッドを呼び出さずに、クラス B、クラス C でオーバーライドしたクラスメソッドを呼び出してみます。

B.class_method_a2        # => "created by class method a2 in B."
C.class_method_a3        # => "created by class method a3 in C."
A.class_variables        # => []
B.class_variables        # => [:@@a2]
C.class_variables        # => [:@@a3, :@@a2]

今度は、クラス A では @@a2@@a3 が認識されていません。クラス B でも、クラス C でオーバーライドしたメソッドで生成した @@a3 が認識されていません。

・・・さらに・・・

もう一度リセットして、クラス C でオーバーライドしていないクラスメソッドを呼び出してみます。

C.class_method_a1        # => "created by class method a1 in A."
C.class_method_a2        # => "created by class method a2 in B."
A.class_variables        # => [:@@a1]
B.class_variables        # => [:@@a2, :@@a1]
C.class_variables        # => [:@@a2, :@@a1]

class_method_a1 は、クラス A からそのまま継承されているので、@@a1 はクラス A で生成されていますが、class_method_a2 の方は、クラス B でオーバーライドしているので、クラス B のクラスメソッドとして実行され、@@a2 はクラス B のクラス変数になってしまっています。

つまり、定義がどのクラスに書かれているか、は関係なく、クラス変数の生成(初期化)が実行されたクラスのクラス変数となる、ということになります。

さらにややこしいこととして、クラス C でクラス変数を生成後、クラス A で同じクラス変数を生成すると。

C.class_method_a3        # => "created by class method a3 in C."
C.class_variable_get :@@a3    # => "created by class method a3 in C."
A.class_variables        # => []
C.class_variables        # => [:@@a3]

A.class_method_a3        # => "created by class method a3 in A."
C.class_variable_get :@@a3    # => "created by class method a3 in A."
A.class_variables        # => [:@@a3]
C.class_variables        # => [:@@a3]

クラス C のクラス変数だった @@a3 が、クラス A のクラス変数に置き換わってしまいます。

このように、クラスの継承やメソッドのオーバーライドが絡んでくると、どこで生成されたクラス変数か分かり辛くなり、クラスメソッドの実行タイミングによっても、クラス変数の影響範囲が変わってしまう、ということになってしまいます。

どうしたらいいか?

ですので、その1でも書いたように、クラス変数の場合はクラス定義内の「メソッドの外」で必ず初期化(生成)を行うことを強く推奨します。これだけで、「継承」の問題は解決できます。