概要
最近またプログラミングをする機会が増えたのでお勉強したことを記しておきます。
C++からC#に浮気しました。
今回はC#とC++の型の扱いの違いについて記していこうと思います。
値型・参照型
C#では型の種類を大きく2つに分けると、値型か参照型となります。
値型はローカル宣言した時に値(中身)を直接スタック上に置く型です。
参照型は中身を直接持たず、値(中身)がある場所、つまり参照情報(アドレス)をスタック上に置く型です。
C#の値の値型/参照型
値型
struct、int、float、double、charなど
参照型
class、string、delegateなど
C++から移行する方はstructが値型でclassが参照型になっていることに留意するとあとあと面倒にならなくて済むかもしれません。
C++の値型/参照型
値型
struct、class、int、float、double...全部?
参照型(ポインタ型も含む)
型名に「&」や「*」がついてるやつ
C#の概念ならC++のプリミティブ型も複合型(class/struct)もローカル宣言するとスタック上に値(中身)が置かれるので値型といえるでしょう。(ただし抽象クラスはローカル宣言できませんが・・・)
C++の参照型は型名に「&」や「*」を修飾することで参照型となります。
C#の「ref」「out」「in」キーワードと用途は似ていますが、ここでいう参照型・値型とちょっと違う概念です。
C++ to C#
class A{
public:
int content=1;
}
があるとします。
C++のクラスは値型なのでnewなどせずに直接スタックに中身を置けます。
C++では以下のように使えます。
Aa;// 値型 中身1そのものAa2;// 値型 中身1そのものa2.content=2;// a2の中身を2にするa=a2;// aにa2の中身をコピーするa.content=3;// aの中身を3にする// この段階で // a.content==3 // a2.content==2
Aはスタックに置かれるので高速です。
しかしC#のクラスは参照型なので中身をスタック上に置くことはできません。
必ずnewをしてヒープ上に値(中身)を置いて、その場所のアドレス(参照)を保持する形になります。
C++で実現できるC#に近い感じのコードは以下のようになります。
A*a=newA();// 参照型 どっかに中身1をつくってその参照(アドレス)をaに格納A*a2=newA();// 参照型 どっかに中身1をつくってその参照(アドレス)をa2に格納a2->content=2;// a2の中身を2にするa=a2;// aにa2が格納している参照(アドレス)をコピーする ←ここがちがう!a->content=3;// aの中身を3にする// この段階で // a->content==3 // a2->content==3 ←aとa2は同じ参照先なので 同じ値になっちゃう
deleteしてないしスマートポインタすら使わないこんなアホなコード書くなとか突込まないでね。趣旨がずれちゃう・・・
C/C++のポインター(型名*)がC#でいう参照型に近いです。
C++の参照型(型名&)はほかの言語では見ない特殊な感じがします。
上記のC++コードと近いC#コードは以下のようになります。
Aa=newA();// 参照型 どっかに中身1をつくってその参照(アドレス)をaに格納Aa2=newA();// 参照型 どっかに中身1をつくってその参照(アドレス)をa2に格納a2.content=2;// a2の中身を2にするa=a2;// aにa2が格納している参照(アドレス)をコピーするa.content=3;// aの中身を3にする// この段階で // a.content==3 // a2.content==3
まーたdeleteも書かないで・・・
いえ、C#ではdeleteキーワードはありません。ガーベッジコレクションという仕組みが自動的にメモリーを開放してくれます。IDisposableが実装されていないならユーザーが明確に管理する必要はありません。というかできないのです。
object型
C♯にはobject型というのがあります。ざっくりというならば、intとかstructとかclassとかいろんな型の参照を入れられる型です。Cのvoidポインタ、C++のstd::anyのようなものが言語仕様で備わっています。
voidポインターと異なるところは、型情報を持っているので間違った型にキャストしようとすると例外なりnullを返すなりして危険なアクセスが起こらない仕組みになっているところです。
std::anyと異なるところは、std::anyはラッパーのようなもので、対象の値や参照を包み込む形になっています。一方、C#のclassはすべて暗黙にobjectを継承しているのでobjectにアップキャストされる形になっています。え?じゃあintとかstructもobjectに入れられるけどintやstrcutはclassじゃないよね??アップキャストできないよね???ってなりますよね、そこにはボックス化(ボクシング)という仕組みが働いています。(そのボックス化の仕組みはstd::anyの仕組みに近いかもしれません。)
C流 なんでもこい型
inta=3;// ごく普通の整数型void*b=&a;// なんでもこいなvoidポインタにaの参照をぶちこむintc=*(int*)b;// voidポインタをintポインタにキャスト cには3が入るb="Hello";// 今度はvoidポインタに文字列(参照)をぶちこんでみるconstchar*d=(constchar*)b;// voidポインタを文字列(参照)にキャスト dには"Hello"が入る
C++流 なんでもこい型
inta=3;// ごく普通の整数型anyb=a;// なんでもこいなanyにaの値をぶちこむintc=any_cast<int>(b);// anyをintにキャスト cには3が入るb=string("Hello");// 今度はanyに文字列をぶちこんでみるstringd=any_cast<string>(b);// anyをstringにキャスト dには"Hello"が入る
C#流 なんでもこい型
inta=3;// ごく普通の整数型objectb=a;// なんでもこいなobjectにaの値をぶちこむ(ボックス化)intc=(int)b;// objectをintにキャスト cには3が入る(ボックス解除)b="Hello";// 今度はobjectに文字列をぶちこんでみる(ボックス化?)stringd=(string)b;// objectをstringにキャスト dには"Hello"が入る(ボックス解除?)
ボックス化
int型は値型でクラスのように型情報を持ちません。
C#は安全な型変換を保証するようになっています。
変換ができない場合は例外を投げるかnullを返すかのどちらかです。
(C/C++のように未定義動作ではない)
そこで型情報を持たせる必要がでてきます。
inta=3;objectb=a;
上記ようにintをobjectに変換しようとすると、以下のように内部でobejctを継承するintのラッパークラスでラップしてしまいます。(イメージ)
classIntWrapper:object{publicintValue;publicIntWrapper(intval){this.Value=val;}}objectb=newIntWrapper(a);
おそらく上記のようなコードに変換されるでしょう(イメージ)。IntWrapperというのは勝手につけた名前ですが、int型の値をラップするクラスだと思ってください。このようにラッパーが生成されラップされてしまうことをボックス化・ボクシングと言います。
intc=(int)b;
上記のように逆にobjectからintに変換するときは、以下のようにIntWrapperにキャストして値を取り出す形になるでしょう。
intc=((IntWrapper)b).Value;
このようにラッパーから値を取り出すことをボックス解除・アンボクッスといいます。
その処理を内部でやってくれるのでコーディング上はとっても汎用的でシームレスでキモチイイ~ので多用してしまいそうですね。
しかし、ボクシングはラッパークラスの生成が起こるわけですから、値型をobjectにつっこむのは、派生クラス参照型をobjectに突っ込むのより処理コストがかかります。
・・・ところでstringは参照型だけどボックス化は起こるのかなぁ?調査不足...
キャストのコスト
派生クラスからobjectのアップキャストは参照情報(アドレス)のコピーぐらいで大したことはありません。ダウンキャストはそれなりにありますが、C++のdynamic_castよりかはるかに速いです。とにかく遅いdynamic_cast... C++コンパイラの実装は多数あるので、もしかしたら早い実装もあるかもしれませんが、大体の場合、型チェックの機構を自分で実装しないと多用が難しいレベルで遅いです。どうやら内部では継承するクラスを線形で、しかもなが~い文字列を一個一個比較していってるようです。それとくらべるとC#のダウンキャストは劇的に速いです。型による分岐処理にswitch・case文も使えます。すごいぞC#