.NET Frameworkの型システムでは、型は値型(value type)と参照型(reference type)の二種類に大別されます。 具体的には、int, float, char, boolなどの基本型・構造体・列挙体などが値型となります。 また、オブジェクト型(object)・文字列型(string)・クラス・インターフェイスなどが参照型となります。
値型はデータに直接アクセスする型で、参照型は参照によってデータの実体にアクセスする型です。 C#やVB.NETなど.NET Frameworkの型システムをベースとする言語でも、値型と参照型の分類が存在します。
値型と参照型
値型と参照型の違い
値型と参照型の違いは値(インスタンス)へのアクセス方法にあります。 値型の変数は常になんらかの値(インスタンス)を格納していて、その値に対して直接アクセスします。 一方、参照型の変数はインスタンスへの参照を格納していて、参照を通して実体(インスタンス)にアクセスします。
また変数への代入時の動作も異なります。 値型の変数に対して代入を行う場合は代入元の値がコピーされて代入されるのに対して、参照型の変数に対して代入を行う場合は参照のみがコピーされて代入されます。 この時、インスタンス自体はコピーされません。
このため、値型では変数がそれぞれ別の値(インスタンス)を持つことになるため同一のインスタンスを参照することはありませんが、参照型では変数に格納される参照しだいでは同一の実体(インスタンス)を参照することもあります。
次のコードではこの違いを明確にしています。 ここで、構造体は値型、クラスは参照型であることを念頭に置いてください。
このように、代入を行った後に代入元(変数a)に変更を加えた場合、値型の場合では代入先(変数b)に影響しないのに対して、参照型の場合では見かけ上代入元への変更は代入先にも影響するような動作となります。
この他にも値型と参照型でいくつかの相違点があり、まとめると次のようになります。
値型 | 参照型 | |
---|---|---|
変数に代入される値 | インスタンス(値)そのもの | インスタンスへの参照 |
代入時の動作 | 値の複製(コピー)が代入される | 参照が代入される |
ヌル参照 | 変数をヌル参照にすることはできない | 変数をヌル参照にすることができる |
デフォルトコンストラクタ | 暗黙的に実装される 明示的に実装することはできない |
必要なら明示的に実装することができる |
インスタンスがアロケートされる場所 | スタック | ヒープ |
インスタンスが破棄されるタイミング | スコープから脱した時点で破棄される | ガベージコレクタにより定期的に破棄される |
値型と参照型の分類
.NET Frameworkの型システムにおいては、値型と参照型のどちらに分類されるかは明確に定められています。 値型・参照型となる型はそれぞれ次のようになります。
-
値型
-
プリミティブ型
- 数値型 (int, float, IntPtr等)
- 文字型 (char)
- ブール型 (bool)
- 構造体
- 列挙体
-
プリミティブ型
-
参照型
- クラス
- インターフェイス
- デリゲート
- オブジェクト型 (object)
- 文字列型 (string)
- 配列
.NET Frameworkの型システムにおいてはint(System.Int32)やfloat(System.Single)などのプリミティブ型も構造体であることから、おおまかに「構造体・列挙体が値型、それ以外が参照型」と分類することができます。 Type.IsValueTypeプロパティを参照することで実行時に型が値型かどうかを知ることができます。
値型・参照型の挙動の違い
以下では値型・参照型の挙動の違いや扱う上での注意点、構造体とクラスの使い分けなどについて解説します。
値渡し・参照渡し
メソッドの引数を値渡しする場合、値型の場合は代入の際と同様インスタンスの複製がメソッドに渡されるため、メソッド内で引数に変更を加えても呼び出し元の変数には反映されません。
一方参照型の場合、メソッドの引数には参照が渡されるため、呼び出し元の変数と同一のインスタンスを参照します。 そのため、メソッド内で引数に変更を加えると呼び出し元の変数に反映されます。
メソッドの引数を参照渡し(ref/ByRef)する場合は、値型の場合でも引数に指定したインスタンスを参照することになるため、メソッド内で引数に変更を加えると参照型の場合と同様に呼び出し元の変数に反映されます。
値型のプロパティ・インデクサ
値型では代入時にコピーが作成されますが、値型のプロパティやインデクサから値を取得しようとする場合も同様にコピーが作成されます。 値型のプロパティ・インデクサはインスタンスそのものではなくインスタンスのコピーを返すことから、直接インスタンスを変更することができません。
そのため、次の例のように値型のプロパティを直接変更しようとするとコンパイルエラーとなります。 参照型では変更しようとするインスタンスを参照によって取得することができるため、コンパイルエラーとはなりません。
コンパイルエラーとならないようにするには、次の例のように一旦プロパティの値を一時変数に代入してコピーし、変更を加えた後にプロパティに再代入します。
値型配列の要素を直接変更することはできますが、値型のインデクサの場合もプロパティと同様に直接変更することはできません。
さらに、インスタンスに変更を加えるようなメソッドを呼び出す場合も同様の問題が発生します。 以下の例ではプロパティで取得した値型インスタンスのメソッドを呼び出していますが、メソッド呼び出しで変更されるのはあくまで取得によってコピーされたインスタンスであって、元のインスタンスには一切影響しないため、一見するとメソッド呼び出しによる変更が反映されないような動作となります。
この場合も、先の例と同様に一時変数に代入してからメソッド呼び出しを行うことで変更を反映させることができます。
一見すると予想に反する動作であるにも関わらず、このようなコードはコンパイルエラーとはならないため注意する必要があります。 インデクサを多用するListやDictionaryなどのジェネリックコレクションで値型を扱う場合にはこういった問題に遭遇しやすいので注意が必要です。
同値性・同一性の比較
Equalsメソッドを使うと、二つのインスタンスが等しいかどうかの比較が行われます。 Equalsメソッドのデフォルトの動作は値型と参照型で次のように異なります。
- 値型の場合
- 二つの値がビット単位で等しい場合にtrueとなる
- 参照型の場合
- 二つの参照が同一のインスタンスを参照している場合にtrueとなる
つまり、値型では同値性の比較が行われ、参照型では同一性の比較が行われます。
EqualsメソッドをオーバーライドしたりIEquatable<T>インターフェイスを実装することでEqualsメソッドの動作を変えることができます。 例えば、String.Equalsメソッドが文字列の同値性の比較を行うように、参照型でも同値性の比較を行うように実装することができます。 このほか、任意の型同士で参照の比較(同一性の比較)を行いたい場合は、Object.ReferenceEqualsメソッドを使うことができます。
ボックス化
値型のインスタンスをobject型変数に代入する場合、スタックに配置されている値型インスタンスの複製が作成され、object型変数に箱詰めした上でヒープに配置されます。 これをボックス化(boxing)と呼びます。 ボックス化の際インスタンスの複製が作成されるため、元のインスタンスとボックス化されたインスタンスは別々のものとなります。 そのため、元のインスタンスに変更を加えてもボックス化されたインスタンスには影響しません。
参照型のインスタンスをobject型変数に代入する場合は単にアップキャストとなるだけで、ボックス化は行われません。 参照がobject型変数に代入されるだけとなるため、当然object型に代入されるインスタンスは元のインスタンスと同一のものとなります。
逆に、object型変数から値型のインスタンスを取り出す際にはボックス化解除(unboxing)が行われます。 ボックス化解除では、object型に箱詰めされている値型インスタンスを取り出して値型変数に代入します。
object型変数への代入だけでなく、値型が実装するインターフェイス型へ代入する場合にもボックス化が行われます。
ボックス化ではインスタンスの複製が作成されるため、参照型のキャストと比べると若干コストのある操作となります。 値型のボックス化・ボックス化解除と参照型のアップキャスト・ダウンキャストの速度を比較すると次のようになります。
非ジェネリックコレクションで値型を扱う場合、コレクションへの格納・取り出しを行う度にボックス化・ボックス化解除されることになります。 そのため、パフォーマンスの観点からもArrayListなどの非ジェネリックコレクションよりもボックス化・ボックス化解除が発生しないListなどのジェネリックコレクションを使うことが推奨されます。
代入の速度
値型の代入ではインスタンスの複製が行われるため、型のサイズが大きくなるほど代入にかかるコストは大きくなります。 参照型ではインスタンスの複製は行われないため、型のサイズによらず代入にかかるコストは一定となります。
サイズの大きい構造体の代入を多数行う必要がある場合、クラスに置き換えることを検討することでコストを下げることができます。 クラスまたは構造体の選択(MSDN)では、構造体とクラスのどちらを選択するかという基準の1つに「サイズが16バイト未満かどうか」というガイドラインが設定されています。
インスタンスの複製 (Object.MemberwiseClone)
Object.MemberwiseCloneメソッドを使ってインスタンスの複製を作成する際、フィールドが値型か参照型かによって複製時の動作が異なります。
値型のフィールドはビット単位での複製(詳細コピー)が行われるのに対し、参照型のフィールドは参照のみが複製されます(簡易コピー)。 そのため、MemberwiseCloneメソッドによるインスタンス複製後の参照型フィールドは、複製元の同一フィールドと同じインスタンスを参照することになります。
インスタンスの複製とMemberwiseCloneメソッド、詳細コピーと簡易コピーの動作についてはオブジェクトの複製 §.Object.MemberwiseCloneによる複製でも解説しています。
デフォルト値
初期値を指定しないでフィールドやローカル変数の宣言を行った場合、デフォルト値で初期化されます。 値型のデフォルト値は0
(もしくは0
に相当する値)で、参照型のデフォルト値はヌル参照(null/Nothing)です。
型のデフォルト値について詳しくは型の種類・サイズ・精度・値域 §.型のデフォルト値を参照してください。
値型か参照型か調べる
実行時に型が値型か参照型かを調べるには、型情報(Type)を取得しIsValueTypeプロパティを参照します。
型情報の取得について、およびTypeクラスについて詳しくはリフレクションで解説しています。
ジェネリック型の制約
ジェネリック型を定義する際、型パラメータに制約(constraints)を持たせることができます。 型パラメータに指定できる型を特定の基本型やインターフェイスを実装する型に限定するのと同様に、値型・参照型のみに限定させることもできます。
C#ではwhere句を使ってwhere T : struct
とすれば型パラメータT
を値型に、where T : class
とすれば参照型に限定することができます。 VBではOf句を使ってOf T As Structure
とすればT
を値型に、Of T As Class
とすれば参照型に限定できます。