C# Tips
−値型と参照型−


[トップ] [目次]

変数の2つの型

C#には(というかCLSには)、値型と参照型という区別があります。 すべての変数は、このどちらかに属します。 いったい、何が違うんでしょう? 答えを先に書くと値型はstructで、参照型はclassです(本当はenumも値型なんですが、ここでは省略します)。 違いはこれだけです。構文上の違いはこれだけなんですが、動作上はいろいろなところが違います。


初期化の違い


1
2
3
4
5
6
7
8
9
10
11
12
13

using System;

public struct SInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        SInt i1;
        i1.i = 1;
        Console.WriteLine(i1.i);
    }
}
値型の初期化

9〜10行目のように値型は変数定義するだけで使えます。
ただ、もちろん、定義だけして初期化しないで使うことはできません。


SInt i1;
Console.WriteLine(i1.i);

こんなことをすると、「error CS0170: フィールド 'i' は、割り当てられていない可能性があります。」ってエラーになります。 そりゃそうです。実際、何も割り当ててないんですから。 割り当ててないものを表示しようったって何が表示されるかわかりません。 こういうとき、C++では大概warningですが、C#コンパイラはerrorになるようです。

対して、参照型は、


1
2
3
4
5
6
7
8
9
10
11
12
13

using System;

public class CInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        CInt i1 = new CInt();
        i1.i = 1;
        Console.WriteLine(i1.i);
    }
}
参照型の初期化

9行目のように使う前に必ずnewしたオブジェクトを代入してやらなければいけません。
よく、「参照型は入れ物にすぎない。入れられる物(オブジェクト)が必ず別に必要」みたいな説明がありますが、そのとおりなんです。
もちろん、こちらも初期化しないで使用することはできません。


CInt i1;
Console.WriteLine(i1.i);

こんなことをすると、「error CS0165: 未割り当てのローカル変数 'i1' が使用されました。」ってエラーになります。
同じ未割り当てでも、エラー番号・エラーの内容ともに、値型と参照型では違ってます。 ちゃんとC#コンパイラはどちらの型なのか判断してるわけです(あたりまえですが)。 まぁ、簡単な話で、単にその型がstructなのかclassなのかを見てるだけです。 で、参照型(class)の場合は、「オブジェクトを入れてやらないと空っぽだから使えないよ」って訳です。

ちなみに、もちろん値型もnewすることができます。


SInt i1 = new SInt();

ただ、参照型のnewとは意味が違います。 ildasmで見ると、SIntを作って、それの内容をi1にコピーしてるようです。newで作ったSIntは内容のコピーが終わったら用無しです。 参照型の場合は「newでオブジェクトを作って、それへの参照を変数に代入する」ということになります。 だから本質的に意味が違うわけです。
確かに、無意味に値型をnewすると、その分だけちょっと余計なコードができてますが、気にすることはないと思います。 "new Point(10, 20)"みたいにコンストラクタの引数で初期化したいためにnewすることも多いですし(Pointはstructです。なので値型です)。


代入の違い


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

using System;

public struct SInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        SInt i1;
        i1.i = 1;
        SInt i2 = i1;
        i2.i = 2;
        Console.WriteLine(i1.i);
        Console.WriteLine(i2.i);
    }
}
値型の代入
  1. 9行目でSInt型(struct)の変数i1を作ります。
  2. 10行目でi1.iに1を代入します。
  3. 11行目でSInt型の変数i2を作り、i1の内容を丸ごと代入します。なので、この時点でi2.iは1です。
  4. 12行目でi2.iに2を代入します。
  5. 13行目で書かれるi1.iの値は1です。
  6. 14行目で書かれるi2.iの値は2です。

こんな風に値型の代入は値のコピーを意味します。
何のことかわからない?じゃあ、下の参照型の場合と比べてみてください。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

using System;

public class CInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        CInt i1 = new CInt();
        i1.i = 1;
        CInt i2 = i1;
        i2.i = 2;
        Console.WriteLine(i1.i);
        Console.WriteLine(i2.i);
    }
}
参照型の代入
  1. 9行目でCInt型(class)の変数i1を作ります。参照型なので必ずnewで作ってやらなくちゃいけません。i1はこのnewされたオブジェクトを参照しているだけです。
  2. 10行目でi1.iに1を代入します。
  3. 11行目でCInt型の変数i2を作り、i1を代入します。代入されるのは9行目でnewしたオブジェクトの参照です。なので、i2はi1と同じ物を参照しています。
  4. 12行目でi2.iに2を代入します。実際にはi2が参照しているオブジェクトのiフィールドへの代入です。
  5. 13行目で書かれるi1.iの値は2です。
  6. 14行目で書かれるi2.iの値は2です。

こんな風に参照型の代入は参照のコピーを意味します。
初期化の違いとあわせて考えればわかると思いますが、値型は変数の数だけ存在するのに対して、参照型はnewした数だけ存在すると考えるとわかりやすいと思います。
ちなみに、参照型の「中身」をコピーしたいときは、CopyやCopyToなどといったメソッドを使います(自分でclassを書くときは当然用意しとかなくちゃいけない)。

注意
「newした数だけ」と書きましたが、暗黙の変換演算子が定義されていると、一見したところnewしていることがわかりません。


public class CInt {
    public int i;
    
    // コンストラクタ
    public CInt() {}
    public CInt(int n) { i = n; }
    
    // int から CInt への暗黙の変換演算子
    public static implicit operator CInt(int n) {
        return new CInt(n);
   }
}

このように暗黙の変換演算子が定義されていれば


CInt i1 = 1;

と書けます。
これはどうなっているかというと、C#コンパイラがコンパイルするときに、

  1. CInt型のi1を作る。
  2. それにint型(数字の1)を代入しようとする。
  3. しかし、当然、CInt型にint型を代入することはできない(型が違うため)。
  4. そこで、C#コンパイラはint型をCInt型に変換する方法を探す。
  5. すると、int型からCInt型に変換するoperator CInt(int)が見つかる。
  6. そこで、operator CIntを呼び出して、返って来た結果(CInt型)をi1に代入するようにコンパイルする。

てな具合になるわけです。
結局、operator CIntの中で"new CInt()"しているので「newした数だけ」というのは変わらないんですが、パッと見はnewしてないように見えます。

それに初期化のところに書いたように値型でもnewできますから、「newしてれば参照型」というわけではありません。 結局、その型がstructかclassかをリファレンスなりで調べないと、値型か参照型かはわからないということになります。


==演算子の違い


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

using System;

public struct SInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        SInt i1;
        SInt i2;
        i1.i = 1;
        i2.i = 1;
        Console.WriteLine(i1 == i2);
    }
}
値型の==演算子

実はこのコードはコンパイルエラーになっちゃいます。 13行目が"i1.i == i2.i"ではなく、"i1 == i2"となっているところに注意してください。 「structどおしが等しいか?」を調べようとしてるんですが、そういうことはサポートされていません。
参考までに書いておきますが、そういうことがしたいときは自分で==演算子をオーバーロードしておかなくちゃいけません (この辺は、下のほうでもういっぺんまとめます)。


public struct SInt {
    public int i;
    
    public static bool operator==(SInt lhs, SInt rhs) {
      return lhs.i == rhs.i;
   }
    public static bool operator!=(SInt lhs, SInt rhs) {
      return lhs.i != rhs.i;
   }
}

こういう風に、operator==とoperator!=をオーバーロードすれば、当然、"i1 == i2"のような比較ができるようになります。 ちなみに、operator==だけオーバーロードしてoperator!=がないと「!=演算子がない」ってコンパイルエラーになります。 C#では、片手落ちは許さないようです。 それに、EqualsメソッドとGetHashCodeメソッドをオーバーライドしてないってwarningになります。 この辺の話はDr. GUI.NET #2などに載ってます。

では、参照型ではどうでしょうか。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

using System;

public class CInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        CInt i1 = new CInt();
        CInt i2 = new CInt();
        i1.i = 1;
        i2.i = 1;
        Console.WriteLine(i1 == i2);
    }
}
参照型の==演算子

参照型の場合は"i1 == i2"も正しくコンパイルできます。 ただ、13行目で表示される結果は"False"です。 i1.iもi2.iも1が入ってるのになぜでしょう? それは、参照型自体を==で比較したときは「同じオブジェクトか?」を比較してるからです。 自動的に「同じ内容か?」を比較してくれるわけではありません。 上の例だと、i1とi2は別々にnewしたオブジェクトなので、当然、「別のオブジェクト」なわけです。 だから"False"になるわけです。


引数で渡すときの違い


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

using System;

public struct SInt {
    public int i;
}

public class Test {
    public static void SFunc(SInt i) {
        i.i = 2;
    }
    
    public static void Main(string[] args) {
        SInt i1;
        i1.i = 1;
        Test.SFunc(i1);
        Console.WriteLine(i1.i);
    }
}
値型を引数で渡す

値型を引数で渡すと文字通り値渡しになります。
これは、15行目でi1をSFuncに渡すときにi1のコピーが作られそれがSFuncに渡されるということです (値だけが渡されるので値渡し)。 だから、9行目でSIntの内容を変更しても、それはコピーの内容を変更するだけです。 コピー元には何も影響を与えません。 このコピーはSFuncからリターンするときに消えてなくなります。 なので、16行目で表示されるのは"1"です。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

using System;

public class CInt {
    public int i;
}

public class Test {
    public static void CFunc(CInt i) {
        i.i = 2;
    }
    
    public static void Main(string[] args) {
        CInt i1 = new CInt();
        i1.i = 1;
        Test.CFunc(i1);
        Console.WriteLine(i1.i);
    }
}
参照型を引数で渡す

参照型を引数で渡すと参照渡しになります。
15行目でi1をCFuncに渡すときにi1のコピーが作られ、それがCFuncに渡されるということは値型と同じです。 ただ、i1に格納されているのは「13行目でnewしたオブジェクトへの参照」です。 これのコピーが渡されるということは、CFuncのiは「13行目でnewしたオブジェクトを参照している」ということになります。 同じ物を参照しているのですから、9行目でSIntの内容を変更すると、それは13行目でnewしたオブジェクトの内容を変更するということになります。 なので、16行目で表示されるのは"2"です。


ボックス化変換とボックス化解除変換

ボックス化変換とは値型を自動的に参照型に変換すること、ボックス化解除変換とはボックス化変換されて参照型になってる値型を元に戻すことです。
こう書くと、なにやら意味不明ですが、別に難しい話しじゃありません。

.NET FrameworkにあるArrayListなどのコンテナはobjectを格納するようになってます。 メソッドの引数で「何でもあり」を表現するときもobjectを使います。
ただ、objectはもちろんclassなので、参照型です。 だから、objectに値型を代入するときは値型から参照型への変換が必要ってことになります。 これが、ボックス化変換です。
結局、コードにすると


SInt i1;
i1.i = 1;
ArrayList ary = new ArrayList();
ary.Add(i1);

こういうことができるようになってるってだけです。
ArrayList.Addメソッドの引数の型はobjectですが、値型でも気にせず渡せるようになってるわけです (このときに、ボックス化変換が実行されています)。

ボックス化解除変換はobjectから元の値型に戻すだけです。


SInt i2 = (SInt)ary[0];

ボックス化解除変換は自動的にはやってくれないので、自分でキャストしなくちゃいけません。 まぁ、これは参照型でも同じですが。

途中にobjectが介在すると、ボックス化変換、ボックス化解除変換が発生するわけですが、挙動としては普通の値型とほとんど同じです。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

using System;

public struct SInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        SInt i1;
        i1.i = 1;
        object o = i1;
        SInt i2 = (SInt)o;
        i1.i = 2;
        i2.i = 3;
        Console.WriteLine(i1.i);
        Console.WriteLine(i2.i);
        Console.WriteLine(((SInt)o).i);
    }
}
ボックス化変換とボックス化解除変換
  1. 9、10行目でi1を1にします。
  2. 11行目でi1をボックス化変換してoに代入します。ボックス化変換時にはi1を参照型に変換したオブジェクトが新たに作られ、oにセットされるのはこのオブジェクトです。
  3. 12行目でoからボックス化解除変換してi2に代入します。ボックス化解除変換のときにも新たに値型のものが作られて、それがi2に代入されます。
  4. 13行目でi1に2を代入してますが、影響を受けるのはi1自身だけです。なぜなら、oはi1を元に新たに作られたオブジェクトですし(i1を参照しているわけではない)、i2もoを元に新たに作られたものだからです。
  5. 14行目でi2に3を代入してますが、これも影響を受けるのはi2自身だけです。
  6. なので、15〜17行目で出力されるのは、i1が2、i2が3、oが1です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

using System;

public class CInt {
    public int i;
}

public class Test {
    public static void Main(string[] args) {
        CInt i1 = new CInt();
        i1.i = 1;
        object o = i1;
        CInt i2 = (CInt)o;
        i1.i = 2;
        i2.i = 3;
        Console.WriteLine(i1.i);
        Console.WriteLine(i2.i);
        Console.WriteLine(((CInt)o).i);
    }
}
参照型のキャスト

先の値型の例とコードはほとんど同じですが、意味は全然違います。

  1. 9、10行目でi1を1にします。
  2. 11行目でi1をoに代入します。すべてのクラスはobjectの子孫なので問題なく代入できます。もちろん、代入されるのは、あくまでも参照です。
  3. 12行目でoをCIntにキャストしてi2に代入します。oに格納されているのはCInt型のオブジェクトなので問題なくキャストできます。もちろん、代入されるのは参照です。
  4. 13行目でi1に2を代入してます。
  5. 14行目でi2に3を代入してます。
  6. 結局、i1もi2もoも同じオブジェクトを参照しています(9行目でnewしたオブジェクト)。なので、15〜17行目で出力されるのは、i1もi2もoも3です。

等値性

等値性っていうのは「値が同じか?」ってことです。 これについて簡単にまとめます。

値型

値型では、operator==はデフォルトでは持っていません。必要に応じて自分で定義する必要があります。 なお、operator==を定義するときはoperator!=も定義しなくちゃいけません。

値型では、Equalsメソッドは「値が同じときtrueを返す」ようになっています。 これは、structは必ずSystem.ValueTypeクラスを親に持ち、ValueTypeクラスのEqualsメソッドがそのような比較をするようになっているためです。 ただ、ValueTypeクラスのEqualsメソッドはあらゆる状況に対応できるようになっているため、かなり複雑です(だそうです)。 なので、普通は自分でオーバーライドすべきです。 もちろん、Equalsメソッドとoperator==は同じ意味を持たせなくてはいけません。
EqualsメソッドをオーバーライドするときはGetHashCodeメソッドもオーバーライドしなくちゃいけません(らしい)。
なお、「C#言語仕様 11.3.2 継承」には「構造体型は暗黙的にクラス object から継承します」と書いてありますが、ildasmで見ると実際にはValueTypeクラスを継承してます。 そうでないと、Equalsメソッドが動く理由が成り立たないため、誤植だと思います。

参照型

参照型では、operator==は「同じオブジェクトを参照しているときtrueを返す」ようになっています。 特殊な場合を除いてoperator==はそのままにしておくべきです。

参照型では、Equalsメソッドは「同じオブジェクトを参照しているときtrueを返す」ようになっています。 ほとんどの場合、そのままにすべきです。 ただし、「値が同じかどうか?」が必要な場合は「値が同じときtrueを返す」ようにオーバーライドします。 EqualsメソッドをオーバーライドするときはGetHashCodeメソッドもオーバーライドしなくちゃいけません。
たとえば、System.VersionクラスやSystem.Drawing.FontクラスなどがEqualsメソッドをオーバーライドして「値が同じかどうか?」を判断できるようになっています。


intも値型

intとInt32はどう違う?に書いたようにintはSystem.Int32クラスのことです。 いや、Int32はクラスではなく構造体(struct)です。 リファレンスにもそう書いてあります。 structなんだから値型です。
自分でstructを作るときも、各種演算子や暗黙の変換演算子なんかを書けば、ほとんどInt32みたいに自然に使える型を作ることができます。 ただ、「C#言語仕様 4.1.3 単純型」にありますがint(==Int32)なんかは構造体型(struct)とは区別してるようです。 さすがに、パフォーマンスにも響くので特別扱いしてるんでしょう。

stringも当然System.Stringクラスです。 こちらはclassです(structではありません)。 ただ、operator==はオーバーロードして「値が同じときtrueを返す」ようになっています。 まぁ、こうしておかないと、


string s1 = "a";
string s2 = "a";
if (s1 == s2) {
    ...
}

これが成り立たなくなっちゃうんで、妥当なところでしょう。


自分で作るときの使い分けは?

自分でクラスを作るときには、値型(struct)とすべきか参照型(class)とすべきかは、どう使い分けするのがいいんでしょう?
C++風に考えると、値型はスタックにとられ、参照型はヒープにとられると思えばいいので、確保・解放を繰り返すときは値型のほうが速いでしょう。 けど、ある程度のサイズを引数で渡したりするときは参照型のほうが速いでしょう(ポインタを渡すだけなので)。 けど、個人的にはそんなにパフォーマンスに影響するのか疑問です。 実測してみないとなんとも言えませんけど、そんなに気にする必要ないのかなと思います。

それよりは、素直に「値を表現するものは値型」としたほうがいいのかと思います。 たとえば、複素数(Complex)とか座標(Point)、色(Color)とかです。 実際、PointとColorは、.NET Frameworkにもstructとして定義されてますし。 結局、「値を表現するもの」は確保・解放・代入なんかが頻繁に行われることが一般的なので、パフォーマンス的にもそれでちょうどよくなるような気がします。


[トップ] [目次]

株式会社ディーバ 青柳 臣一
2002/04/13