LoginSignup
4
1

More than 1 year has passed since last update.

クラス変数の持ち主

Last updated at Posted at 2022-02-24

これは何?

クラス変数の持ち主が、 python と ruby で違うことに気がついたので、他の言語どうなってるんだろと思って調査したのでその記録。ジェネリクスとテンプレートを交えながら。

ruby の場合

ruby
class B
    def set_v(x); @@v=x;  end
    def v(); @@v; end
end

class C < B;end
class D < B;end

c = C.new.set_v("C#set_v")
d = D.new.set_v("D#set_v")
p [ C.new.v, D.new.v ] #=> ["D#set_v", "D#set_v"]

クラス変数 @@v の持ち主は、 class B

C 経由で変更しても、 D から見た @@v の値が変わる。

Python3 の場合

python3
class B:
    v = "in-class-def"
    @classmethod
    def setV(cls,x):
        cls.v = x

class C(B):...

class D(B):...

print(B().v, C().v, D().v) #=> in-class-def in-class-def in-class-def
C().setV("C.setV")
D().setV("D.setV")
print(B().v, C().v, D().v) #=> in-class-def C.setV D.setV

Python のクラス変数はcls.v のようにするので、v の持ち主は cls で指定される型。なので、クラス変数を参照しているメソッドが書かれているクラスとは関係がない。

最初の print 内にある C.v なんかは、C の基底クラスを見に行っている。

わりと珍しい作戦だと思う。

この作戦だと、深い継承ツリーで偶然同じ名前を使ってしまったために大惨事、みたいなことが起きると思う。

※ 初出時間違ったことを書いてましたすいません。

Java の場合

Generics なしで

まずは Generics なしで。

java
import java.io.*;

class B{ public static String s; }
class C1 extends B {};
class C2 extends B {};

class Foo
{
	public static void main (String[] args) throws java.lang.Exception
	{
        C1.s = "c1";
        C2.s = "c2";
        System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2
	}
}

ここは ruby と同じく、クラス変数の持ち主は、その変数が定義されたクラス。

Generics を使うと

Generics を使って、型引数を変えてみると

java
import java.io.*;

class B<T>{ public static String s; T dummy; }
class C1 extends B<Integer> {};
class C2 extends B<Long> {};

class Foo
{
    public static void main (String[] args) throws java.lang.Exception
    {
        C1.s = "c1";
        C2.s = "c2";
        System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2
    }
}

異なる型引数でもクラス変数は同じだということがわかる。
なので、型引数の型を static 変数には使えない。なるほどこれが型消去かという感じ。

C++ の場合

テンプレートなし

まずはテンプレートなしで。

c++17
#include <iostream>
#include <string>

struct B{ static inline std::string s; };
struct C1 : public B{};
struct C2 : public B{};

int main(){
    C1{}.s = "c1";
    C2{}.s = "c2";
    std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c2 c2
    return 0;
}

ruby, Java と同様、static変数(クラス変数) の持ち主は、その変数が定義されたクラス。

テンプレートあり

テンプレートクラスの場合。

c++17
#include <iostream>
#include <string>

template<int n>
struct B{ static inline std::string s; };
struct C1 : public B<0>{};
struct C2 : public B<1>{};

int main(){
    C1{}.s = "c1";
    C2{}.s = "c2";
    std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c1 c2
    return 0;
}

テンプレート引数が異なるクラスは別クラスになる。なので、Java と違って static 変数の型にテンプレート引数が使える。

C# の場合

Generics なし

C#
using System;

class B{ public static String s; }
class C1 : B{};
class C2 : B{};

public class Test
{
	public static void Main()
	{
        C1.s = "c1";
        C2.s = "c2";
        Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c2 C2.s=c2
	}
}

まあそうだよね。Java と同じ。

Generics あり

Generics ありだと

C#
using System;

class B<T>{ public static String s;}
class C1 : B<int>{};
class C2 : B<long>{};

public class Test
{
	public static void Main()
	{
        C1.s = "c1";
        C2.s = "c2";
        Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c1 C2.s=c2
	}
}

Java とは異なる。

まとめ

ひとくちに「クラス変数」というけれど、言語によって意味が違うので要注意。

Python はたぶん異端で、明示されているレシーバがそのクラス変数の持ち主。基底クラス内の cls.class_var は、 cls が異なれば異なる変数。cls 指すクラスの別のスーパークラスで同名のクラス変数を使うと同じ変数となる。

ruby, Java, C++, C# は、クラス変数が定義されたクラスがクラス変数の持ち主。
Generics / template を使う場合。
型引数が違っても同じクラス、というのが Java の立場。
template 引数 / 型引数が違うクラスは別のクラス、というのが C++ と C# の立場。

他に試すべき言語あるかなぁ。

余談

今は亡き J# でジェネリクス使ったらどうなるんだろ。
そもそも J# でジェネリクス使えるかどうかも知らないけど。


以下、投稿の翌日に追加。

Python と ruby のクラス変数の気持ち悪いところ

コメントいただいたり他に試したりして、何が気持ち悪いのかわかってきた。

Python の場合

シンプルな例

まずは普通っぽい例。

Python3
class B:v="B0"
class C(B):...
class D(B):...

# [1]
print(B.v, C.v, D.v) #=> "B0 B0 B0"

# [2]
C.v = "C0"
print(B.v, C.v, D.v) #=> "B0 C0 B0"

[1] の print 内の C.v は、 Bv のことで、
[2] の C.v="C0"C.v は、 Cv のことなので、C.v="C0"D.v で得られる値には影響を与えない。

同じ C.v という式だけど、参照するときと代入の左辺になるときで意味が違うのがわかりにくい。とはいえ、オブジェクト指向とはそのようなものだとも思う。

わかりにくい現象

このわかりにくさと Python の += なんかのわかりにくさが合体して、こんなわかりにくいことが起こる。

Python3
class B:
    a=["a"]
    b=["b"]
    c = "c"
    d = "d"

class C(B):...

print(repr([B.a, B.b, B.c, B.d])) #=> [['a'], ['b'], 'c', 'd']

C.a += ["+="]
C.b = C.b + ["+"]
C.c += "+="
C.d = C.d + "+"

print(repr([B.a, B.b, B.c, B.d])) #> [['a', '+='], ['b'], 'c', 'd']

リストの += は新たなオブジェクトは作られないので基底クラスの B.a が書き換わり。
文字列の += は新たなオブジェクトが作られるので代入と同じことになるから B.c は書き換わらない。

かなり気持ち悪い動作だと思うけど、なんでそうなるのかは説明できる感じ。

ruby の場合

シンプルな例

一方 ruby の普通っぽい例を実行すると。

ruby
class B
    def set_vb(x); @@v=x;  end
    def vb(); @@v; end
end

class C < B
    def set_vc(x); @@v=x;  end
    def vc(); @@v; end
end

class D < B
    def vd(); @@v; end
end

B.new.set_vb("B0")
p [B.new.vb, C.new.vc, D.new.vd] #=> ["B0", "B0", "B0"]

C.new.set_vc("C0")
p [B.new.vb, C.new.vc, D.new.vd] #=> ["C0", "C0", "C0"]

上記の通り Python とは違う。
class C 内の @@v を書き換えると class B 内で定義された @@v が書き換わる。なので、C の派生クラスでも基底スラスでもない D から見える @@v にも影響を与える。

ここだけ見るとそれはそれでありかなと思うけれど。

わかりにくい現象

ならば、派生で @@v を作ってから基底で @@v に代入すると

ruby
class B
    def set_vb(x); @@v=x;  end
    def vb(); (defined? @@v) ? @@v : :undefined; end
end

class C < B
    def set_vc(x); @@v=x;  end
    def vc(); (defined? @@v) ? @@v : :undefined; end
end

# [1]
C.new.set_vc("C0")
p [B.new.vb, C.new.vc] #=> [:undefined, "C0"]

# [2]
B.new.set_vb("B0")
p [B.new.vb, C.new.vc] 
# ruby 2.7.5 では、["B0", "B0"]
# ruby 3.1.0 では例外。class variable @@v of C is overtaken by B 

なんと、 ruby 3.1 では例外。
2.7.5 では、基底クラスの @@v を書き換えると派生クラスの @@v が書き換わる。難しい。難しすぎるので例外にしたんだろうと思う。

あるいは。
以下を実行すると。

ruby
module B
    @@x = :bx
    def self.b()
        [   (defined? @@x) ? @@x : :undef,
            (defined? @@y) ? @@y : :undef,
            (defined? @@z) ? @@z : :undef]
    end
end

module C
    @@y = :cy
    def self.c()
        [   (defined? @@x) ? @@x : :undef,
            (defined? @@y) ? @@y : :undef,
            (defined? @@z) ? @@z : :undef]
    end
end

class D
    include B, C
    @@x = :dx
    @@y = :dy
    @@z = :dz
    def self.d(); 
        [   (defined? @@x) ? @@x : :undef,
            (defined? @@y) ? @@y : :undef,
            (defined? @@z) ? @@z : :undef]
    end
end

p [B.b, C.c, D.d]
#=> [[:dx, :undef, :undef], [:undef, :dy, :undef], [:dx, :dy, :dz]]

module B
    @@x = :bx2
    @@y = :by2
    @@z = :bz2
end

p [B.b, C.c, D.d] # 3.0.3 だとここで例外
#=> 3.1.0: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :dz]]
#=> 2.7.5: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :bz2]]

module C
    @@x = :cx2
    @@y = :cy2
    @@z = :cz2
end

p [B.b, C.c, D.d]
#=> 3.1.0: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:bx2, :cy2, :dz]]
#=> 2.7.5: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:cx2, :cy2, :cz2]]

なななんと、2.7.5 でも 3.1.0 でも例外にならずに完走するが、結果が違う。
びっくりした。
3.1.0 だと完走するのに、3.0.3 だと例外。さらにびっくりした。

びっくりしたし、それぞれどんな理路でそのような出力になったり例外になったりするのかは私には説明できない。
2.7.5 と 3.1.0 で結果が違うことに気づいた時点で真剣に考えるのを放棄したということでもあるけれど。

再度まとめ

Python と ruby のクラス変数は、両方ともわりと思いがけないことが起こる。
どちらも気持ち悪いけど、どちらかというとというかわりと圧倒的に ruby の方が気持ち悪い。

ruby のクラス変数はめちゃくちゃ気持ち悪いし、中の人も悩んでいるんだと思うので、継承が絡む局面での利用は避けたほうがいいと思う。

4
1
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1