C#には(というかCLSには)、値型と参照型という区別があります。 すべての変数は、このどちらかに属します。 いったい、何が違うんでしょう? 答えを先に書くと値型はstructで、参照型はclassです(本当はenumも値型なんですが、ここでは省略します)。 違いはこれだけです。構文上の違いはこれだけなんですが、動作上はいろいろなところが違います。
|
|
9〜10行目のように値型は変数定義するだけで使えます。
ただ、もちろん、定義だけして初期化しないで使うことはできません。
|
こんなことをすると、「error CS0170: フィールド 'i' は、割り当てられていない可能性があります。」ってエラーになります。 そりゃそうです。実際、何も割り当ててないんですから。 割り当ててないものを表示しようったって何が表示されるかわかりません。 こういうとき、C++では大概warningですが、C#コンパイラはerrorになるようです。
対して、参照型は、
|
|
9行目のように使う前に必ずnewしたオブジェクトを代入してやらなければいけません。
よく、「参照型は入れ物にすぎない。入れられる物(オブジェクト)が必ず別に必要」みたいな説明がありますが、そのとおりなんです。
もちろん、こちらも初期化しないで使用することはできません。
|
こんなことをすると、「error CS0165: 未割り当てのローカル変数 'i1' が使用されました。」ってエラーになります。
同じ未割り当てでも、エラー番号・エラーの内容ともに、値型と参照型では違ってます。
ちゃんとC#コンパイラはどちらの型なのか判断してるわけです(あたりまえですが)。
まぁ、簡単な話で、単にその型がstructなのかclassなのかを見てるだけです。
で、参照型(class)の場合は、「オブジェクトを入れてやらないと空っぽだから使えないよ」って訳です。
ちなみに、もちろん値型もnewすることができます。
|
ただ、参照型のnewとは意味が違います。
ildasmで見ると、SIntを作って、それの内容をi1にコピーしてるようです。newで作ったSIntは内容のコピーが終わったら用無しです。
参照型の場合は「newでオブジェクトを作って、それへの参照を変数に代入する」ということになります。
だから本質的に意味が違うわけです。
確かに、無意味に値型をnewすると、その分だけちょっと余計なコードができてますが、気にすることはないと思います。
"new Point(10, 20)"みたいにコンストラクタの引数で初期化したいためにnewすることも多いですし(Pointはstructです。なので値型です)。
|
|
こんな風に値型の代入は値のコピーを意味します。
何のことかわからない?じゃあ、下の参照型の場合と比べてみてください。
|
|
こんな風に参照型の代入は参照のコピーを意味します。
初期化の違いとあわせて考えればわかると思いますが、値型は変数の数だけ存在するのに対して、参照型はnewした数だけ存在すると考えるとわかりやすいと思います。
ちなみに、参照型の「中身」をコピーしたいときは、CopyやCopyToなどといったメソッドを使います(自分でclassを書くときは当然用意しとかなくちゃいけない)。
注意
「newした数だけ」と書きましたが、暗黙の変換演算子が定義されていると、一見したところnewしていることがわかりません。
|
このように暗黙の変換演算子が定義されていれば
|
と書けます。
これはどうなっているかというと、C#コンパイラがコンパイルするときに、
てな具合になるわけです。
結局、operator CIntの中で"new CInt()"しているので「newした数だけ」というのは変わらないんですが、パッと見はnewしてないように見えます。
それに初期化のところに書いたように値型でもnewできますから、「newしてれば参照型」というわけではありません。 結局、その型がstructかclassかをリファレンスなりで調べないと、値型か参照型かはわからないということになります。
|
|
実はこのコードはコンパイルエラーになっちゃいます。
13行目が"i1.i == i2.i"ではなく、"i1 == i2"となっているところに注意してください。
「structどおしが等しいか?」を調べようとしてるんですが、そういうことはサポートされていません。
参考までに書いておきますが、そういうことがしたいときは自分で==演算子をオーバーロードしておかなくちゃいけません
(この辺は、下のほうでもういっぺんまとめます)。
|
こういう風に、operator==とoperator!=をオーバーロードすれば、当然、"i1 == i2"のような比較ができるようになります。 ちなみに、operator==だけオーバーロードしてoperator!=がないと「!=演算子がない」ってコンパイルエラーになります。 C#では、片手落ちは許さないようです。 それに、EqualsメソッドとGetHashCodeメソッドをオーバーライドしてないってwarningになります。 この辺の話はDr. GUI.NET #2などに載ってます。
では、参照型ではどうでしょうか。
|
|
参照型の場合は"i1 == i2"も正しくコンパイルできます。 ただ、13行目で表示される結果は"False"です。 i1.iもi2.iも1が入ってるのになぜでしょう? それは、参照型自体を==で比較したときは「同じオブジェクトか?」を比較してるからです。 自動的に「同じ内容か?」を比較してくれるわけではありません。 上の例だと、i1とi2は別々にnewしたオブジェクトなので、当然、「別のオブジェクト」なわけです。 だから"False"になるわけです。
|
|
値型を引数で渡すと文字通り値渡しになります。
これは、15行目でi1をSFuncに渡すときにi1のコピーが作られそれがSFuncに渡されるということです
(値だけが渡されるので値渡し)。
だから、9行目でSIntの内容を変更しても、それはコピーの内容を変更するだけです。
コピー元には何も影響を与えません。
このコピーはSFuncからリターンするときに消えてなくなります。
なので、16行目で表示されるのは"1"です。
|
|
参照型を引数で渡すと参照渡しになります。
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に値型を代入するときは値型から参照型への変換が必要ってことになります。
これが、ボックス化変換です。
結局、コードにすると
|
こういうことができるようになってるってだけです。
ArrayList.Addメソッドの引数の型はobjectですが、値型でも気にせず渡せるようになってるわけです
(このときに、ボックス化変換が実行されています)。
ボックス化解除変換はobjectから元の値型に戻すだけです。
|
ボックス化解除変換は自動的にはやってくれないので、自分でキャストしなくちゃいけません。 まぁ、これは参照型でも同じですが。
途中にobjectが介在すると、ボックス化変換、ボックス化解除変換が発生するわけですが、挙動としては普通の値型とほとんど同じです。
|
|
|
|
先の値型の例とコードはほとんど同じですが、意味は全然違います。
等値性っていうのは「値が同じか?」ってことです。 これについて簡単にまとめます。
値型では、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とInt32はどう違う?に書いたようにintはSystem.Int32クラスのことです。
いや、Int32はクラスではなく構造体(struct)です。
リファレンスにもそう書いてあります。
structなんだから値型です。
自分でstructを作るときも、各種演算子や暗黙の変換演算子なんかを書けば、ほとんどInt32みたいに自然に使える型を作ることができます。
ただ、「C#言語仕様 4.1.3 単純型」にありますがint(==Int32)なんかは構造体型(struct)とは区別してるようです。
さすがに、パフォーマンスにも響くので特別扱いしてるんでしょう。
stringも当然System.Stringクラスです。 こちらはclassです(structではありません)。 ただ、operator==はオーバーロードして「値が同じときtrueを返す」ようになっています。 まぁ、こうしておかないと、
|
これが成り立たなくなっちゃうんで、妥当なところでしょう。
自分でクラスを作るときには、値型(struct)とすべきか参照型(class)とすべきかは、どう使い分けするのがいいんでしょう?
C++風に考えると、値型はスタックにとられ、参照型はヒープにとられると思えばいいので、確保・解放を繰り返すときは値型のほうが速いでしょう。
けど、ある程度のサイズを引数で渡したりするときは参照型のほうが速いでしょう(ポインタを渡すだけなので)。
けど、個人的にはそんなにパフォーマンスに影響するのか疑問です。
実測してみないとなんとも言えませんけど、そんなに気にする必要ないのかなと思います。
それよりは、素直に「値を表現するものは値型」としたほうがいいのかと思います。 たとえば、複素数(Complex)とか座標(Point)、色(Color)とかです。 実際、PointとColorは、.NET Frameworkにもstructとして定義されてますし。 結局、「値を表現するもの」は確保・解放・代入なんかが頻繁に行われることが一般的なので、パフォーマンス的にもそれでちょうどよくなるような気がします。