.NET Frameworkの型システムでは、型は値型(value type)と参照型(reference type)の二種類に大別されます。 具体的には、int, float, char, boolなどの基本型・構造体・列挙体などが値型となります。 また、オブジェクト型(object)・文字列型(string)・クラス・インターフェイスなどが参照型となります。
値型はデータに直接アクセスする型で、参照型は参照によってデータの実体にアクセスする型です。 C#やVB.NETなど.NET Frameworkの型システムをベースとする言語でも、値型と参照型の分類が存在します。
値型と参照型
値型と参照型の違い
値型と参照型の違いは値(インスタンス)へのアクセス方法にあります。 値型の変数は常になんらかの値(インスタンス)を格納していて、その値に対して直接アクセスします。 一方、参照型の変数はインスタンスへの参照を格納していて、参照を通して実体(インスタンス)にアクセスします。
また変数への代入時の動作も異なります。 値型の変数に対して代入を行う場合は代入元の値がコピーされて代入されるのに対して、参照型の変数に対して代入を行う場合は参照のみがコピーされて代入されます。 この時、インスタンス自体はコピーされません。
このため、値型では変数がそれぞれ別の値(インスタンス)を持つことになるため同一のインスタンスを参照することはありませんが、参照型では変数に格納される参照しだいでは同一の実体(インスタンス)を参照することもあります。
次のコードではこの違いを明確にしています。 ここで、構造体は値型、クラスは参照型であることを念頭に置いてください。
using System;
// 構造体(値型)
struct ValType {
public int ID;
public ValType(int id)
{
ID = id;
}
}
class Sample {
static void Main()
{
ValType a = new ValType(1);
// aをbに代入
ValType b = a;
// aの内容が複製されてbに代入されるため、
// aとbは異なる実体を持つ
Console.WriteLine("a.ID = {0}, b.ID = {1}",
a.ID, b.ID);
// aに変更を加える
a.ID = 2;
// aとbは異なる実体であるため、aへの変更は
// bには影響しない
Console.WriteLine("a.ID = {0}, b.ID = {1}",
a.ID, b.ID);
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Structure
Class Sample
Shared Sub Main()
Dim a As New ValType(1)
' aをbに代入
Dim b As ValType = a
' aの内容が複製されてbに代入されるため、
' aとbは異なる実体を持つ
Console.WriteLine("a.ID = {0}, b.ID = {1}", _
a.ID, b.ID)
' aに変更を加える
a.ID = 2
' aとbは異なる実体であるため、aへの変更は
' bには影響しない
Console.WriteLine("a.ID = {0}, b.ID = {1}", _
a.ID, b.ID)
End Sub
End Class
a.ID = 1, b.ID = 1 a.ID = 2, b.ID = 1
using System;
// クラス(参照型)
class RefType {
public int ID;
public RefType(int id)
{
ID = id;
}
}
class Sample {
static void Main()
{
RefType a = new RefType(1);
// aをbに代入
RefType b = a;
// (aのインスタンスへの参照がbに代入されるため、
// aとbは同一の実体を参照する)
Console.WriteLine("a.ID = {0}, b.ID = {1}",
a.ID, b.ID);
// aに変更を加える
a.ID = 2;
// aとbは同一の実体を参照するため、aの実体への
// 変更はbの実体に変更を加えるのと同じ事となる
Console.WriteLine("a.ID = {0}, b.ID = {1}",
a.ID, b.ID);
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Class
Class Sample
Shared Sub Main()
Dim a As New RefType(1)
' aをbに代入
Dim b As RefType = a
' (aのインスタンスへの参照がbに代入されるため、
' aとbは同一の実体を参照する)
Console.WriteLine("a.ID = {0}, b.ID = {1}", _
a.ID, b.ID)
' aに変更を加える
a.ID = 2
' aとbは同一の実体を参照するため、aの実体への
' 変更はbの実体に変更を加えるのと同じ事となる
Console.WriteLine("a.ID = {0}, b.ID = {1}", _
a.ID, b.ID)
End Sub
End Class
a.ID = 1, b.ID = 1 a.ID = 2, b.ID = 2
このように、代入を行った後に代入元(変数a)に変更を加えた場合、値型の場合では代入先(変数b)に影響しないのに対して、参照型の場合では見かけ上代入元への変更は代入先にも影響するような動作となります。
この他にも値型と参照型でいくつかの相違点があり、まとめると次のようになります。
値型 | 参照型 | |
---|---|---|
変数に代入される値 | インスタンス(値)そのもの | インスタンスへの参照 |
代入時の動作 | 値の複製(コピー)が代入される | 参照が代入される |
ヌル参照 | 変数をヌル参照にすることはできない | 変数をヌル参照にすることができる |
デフォルトコンストラクタ | 暗黙的に実装される 明示的に実装することはできない |
必要なら明示的に実装することができる |
インスタンスがアロケートされる場所 | スタック | ヒープ |
インスタンスが破棄されるタイミング | スコープから脱した時点で破棄される | ガベージコレクタにより定期的に破棄される |
値型と参照型の分類
.NET Frameworkの型システムにおいては、値型と参照型のどちらに分類されるかは明確に定められています。 値型・参照型となる型はそれぞれ次のようになります。
-
値型
-
プリミティブ型
- 数値型 (int, float, IntPtr等)
- 文字型 (char)
- ブール型 (bool)
- 構造体
- 列挙体
-
プリミティブ型
-
参照型
- クラス
- インターフェイス
- デリゲート
- オブジェクト型 (object)
- 文字列型 (string)
- 配列
.NET Frameworkの型システムにおいてはint(System.Int32)やfloat(System.Single)などのプリミティブ型も構造体であることから、おおまかに「構造体・列挙体が値型、それ以外が参照型」と分類することができます。 Type.IsValueTypeプロパティを参照することで実行時に型が値型かどうかを知ることができます。
値型・参照型の挙動の違い
以下では値型・参照型の挙動の違いや扱う上での注意点、構造体とクラスの使い分けなどについて解説します。
値渡し・参照渡し
メソッドの引数を値渡しする場合、値型の場合は代入の際と同様インスタンスの複製がメソッドに渡されるため、メソッド内で引数に変更を加えても呼び出し元の変数には反映されません。
一方参照型の場合、メソッドの引数には参照が渡されるため、呼び出し元の変数と同一のインスタンスを参照します。 そのため、メソッド内で引数に変更を加えると呼び出し元の変数に反映されます。
using System;
// 構造体(値型)
struct ValType {
public int ID;
public ValType(int id)
{
ID = id;
}
}
class Sample {
static void Method(ValType v)
{
// 引数で渡される値に変更を加える
v.ID = 2;
}
static void Main()
{
ValType v = new ValType(1);
Console.WriteLine("v.ID = {0}", v.ID);
Method(v);
Console.WriteLine("v.ID = {0}", v.ID);
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Structure
Class Sample
Shared Sub Method(ByVal v As ValType)
' 引数で渡される値に変更を加える
v.ID = 2
End Sub
Shared Sub Main()
Dim v As New ValType(1)
Console.WriteLine("v.ID = {0}", v.ID)
Method(v)
Console.WriteLine("v.ID = {0}", v.ID)
End Sub
End Class
v.ID = 1 v.ID = 1
using System;
// クラス(参照型)
class RefType {
public int ID;
public RefType(int id)
{
ID = id;
}
}
class Sample {
static void Method(RefType r)
{
// 引数で渡されるインスタンスに変更を加える
r.ID = 2;
}
static void Main()
{
RefType r = new RefType(1);
Console.WriteLine("r.ID = {0}", r.ID);
Method(r);
Console.WriteLine("r.ID = {0}", r.ID);
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Class
Class Sample
Shared Sub Method(ByVal r As RefType)
' 引数で渡されるインスタンスに変更を加える
r.ID = 2
End Sub
Shared Sub Main()
Dim r As New RefType(1)
Console.WriteLine("r.ID = {0}", r.ID)
Method(r)
Console.WriteLine("r.ID = {0}", r.ID)
End Sub
End Class
r.ID = 1 r.ID = 2
メソッドの引数を参照渡し(ref/ByRef)する場合は、値型の場合でも引数に指定したインスタンスを参照することになるため、メソッド内で引数に変更を加えると参照型の場合と同様に呼び出し元の変数に反映されます。
using System;
// 構造体(値型)
struct ValType {
public int ID;
public ValType(int id)
{
ID = id;
}
}
class Sample {
static void Method(ref ValType v)
{
// 引数で渡される値に変更を加える
v.ID = 2;
}
static void Main()
{
ValType v = new ValType(1);
Console.WriteLine("v.ID = {0}", v.ID);
Method(ref v);
Console.WriteLine("v.ID = {0}", v.ID);
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Structure
Class Sample
Shared Sub Method(ByRef v As ValType)
' 引数で渡される値に変更を加える
v.ID = 2
End Sub
Shared Sub Main()
Dim v As New ValType(1)
Console.WriteLine("v.ID = {0}", v.ID)
Method(v)
Console.WriteLine("v.ID = {0}", v.ID)
End Sub
End Class
v.ID = 1 v.ID = 2
using System;
// クラス(参照型)
class RefType {
public int ID;
public RefType(int id)
{
ID = id;
}
}
class Sample {
static void Method(ref RefType r)
{
// 引数で渡されるインスタンスに変更を加える
r.ID = 2;
}
static void Main()
{
RefType r = new RefType(1);
Console.WriteLine("r.ID = {0}", r.ID);
Method(ref r);
Console.WriteLine("r.ID = {0}", r.ID);
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Class
Class Sample
Shared Sub Method(ByRef r As RefType)
' 引数で渡されるインスタンスに変更を加える
r.ID = 2
End Sub
Shared Sub Main()
Dim r As New RefType(1)
Console.WriteLine("r.ID = {0}", r.ID)
Method(r)
Console.WriteLine("r.ID = {0}", r.ID)
End Sub
End Class
r.ID = 1 r.ID = 2
値型のプロパティ・インデクサ
値型では代入時にコピーが作成されますが、値型のプロパティやインデクサから値を取得しようとする場合も同様にコピーが作成されます。 値型のプロパティ・インデクサはインスタンスそのものではなくインスタンスのコピーを返すことから、直接インスタンスを変更することができません。
そのため、次の例のように値型のプロパティを直接変更しようとするとコンパイルエラーとなります。 参照型では変更しようとするインスタンスを参照によって取得することができるため、コンパイルエラーとはなりません。
using System;
// 構造体(値型)
struct ValType {
public int ID;
}
class C {
ValType v = new ValType();
// 値型のプロパティ
public ValType V {
get { return v; }
set { v = value; }
}
public override string ToString()
{
return string.Format("v.ID = {0}", v.ID);
}
}
class Sample {
static void Main()
{
C c = new C();
Console.WriteLine(c);
// 値型のプロパティに変更を加えようとする
c.V.ID = 3;
// error CS1612: 変数ではないため、
// 'C.V' の戻り値を変更できません。
Console.WriteLine(c);
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
End Structure
Class C
Dim _v As ValType = New ValType()
' 値型のプロパティ
Public Property V() As ValType
Get
Return _v
End Get
Set (ByVal value As ValType)
_v = value
End Set
End Property
Public Overrides Function ToString() As String
Return String.Format("v.ID = {0}", v.ID)
End Function
End Class
Class Sample
Shared Sub Main()
Dim c As New C()
Console.WriteLine(c)
' 値型のプロパティに変更を加えようとする
c.V.ID = 3
' error BC30068: Expression は値であるため、
' 代入式のターゲットにすることはできません。
Console.WriteLine(c)
End Sub
End Class
(コンパイルエラーとなるため実行できない)
using System;
// クラス(参照型)
class RefType {
public int ID;
}
class C {
RefType r = new RefType();
// 参照型のプロパティ
public RefType R {
get { return r; }
/* setterは必要ない */
}
public override string ToString()
{
return string.Format("r.ID = {0}", r.ID);
}
}
class Sample {
static void Main()
{
C c = new C();
Console.WriteLine(c);
// 参照型のプロパティに変更を加える
c.R.ID = 3;
// (これはコンパイルエラーとはならない)
Console.WriteLine(c);
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
End Class
Class C
Dim _r As RefType = New RefType()
' 参照型のプロパティ
Public ReadOnly Property R() As RefType
Get
Return _r
End Get
'
' setterは必要ない
'
End Property
Public Overrides Function ToString() As String
Return String.Format("r.ID = {0}", r.ID)
End Function
End Class
Class Sample
Shared Sub Main()
Dim c As New C()
Console.WriteLine(c)
' 参照型のプロパティに変更を加える
c.R.ID = 3
' (これはコンパイルエラーとはならない)
Console.WriteLine(c)
End Sub
End Class
r.ID = 0 r.ID = 3
コンパイルエラーとならないようにするには、次の例のように一旦プロパティの値を一時変数に代入してコピーし、変更を加えた後にプロパティに再代入します。
class Sample {
static void Main()
{
C c = new C();
Console.WriteLine(c);
// 現在のプロパティの値を一時変数にコピーする
ValType v = c.V;
// 一時変数に代入したインスタンスに対して変更を加える
v.ID = 3;
// 変更した一時変数のインスタンスをプロパティにコピーする
c.V = v;
Console.WriteLine(c);
}
}
Class Sample
Shared Sub Main()
Dim c As New C()
Console.WriteLine(c)
' 現在のプロパティの値を一時変数にコピーする
Dim v As ValType = c.V
' 一時変数に代入したインスタンスに対して変更を加える
v.ID = 3
' 変更した一時変数のインスタンスをプロパティにコピーする
c.V = v
Console.WriteLine(c)
End Sub
End Class
v.ID = 0 v.ID = 3
値型配列の要素を直接変更することはできますが、値型のインデクサの場合もプロパティと同様に直接変更することはできません。
using System;
using System.Collections.Generic;
// 構造体(値型)
struct ValType {
public int ID;
}
class Sample {
static void Main()
{
ValType[] arr = new ValType[1];
// 配列の要素を変更
arr[0].ID = 3;
List<ValType> list = new List<ValType>(new ValType[1]);
// インデクサを使用して要素を変更
list[0].ID = 3;
// error CS1612: 変数ではないため、'System.Collections.Generic.List<ValType>.this[int]'の戻り値を変更できません。
}
}
Imports System
Imports System.Collections.Generic
' 構造体(値型)
Structure ValType
Public ID As Integer
End Structure
Class Sample
Shared Sub Main()
Dim arr(0) As ValType
' 配列の要素を変更
arr(0).ID = 3
Dim list As New List(Of ValType)()
list.Add(New ValType())
' インデクサを使用して要素を変更
list(0).ID = 3
' error BC30068: Expression は値であるため、代入式のターゲットにすることはできません。
End Sub
End Class
さらに、インスタンスに変更を加えるようなメソッドを呼び出す場合も同様の問題が発生します。 以下の例ではプロパティで取得した値型インスタンスのメソッドを呼び出していますが、メソッド呼び出しで変更されるのはあくまで取得によってコピーされたインスタンスであって、元のインスタンスには一切影響しないため、一見するとメソッド呼び出しによる変更が反映されないような動作となります。
using System;
// 構造体(値型)
struct ValType {
public int ID;
// IDフィールドの値を設定するメソッド
public void SetID(int newID)
{
ID = newID;
}
}
class C {
ValType v = new ValType();
// 値型のプロパティ
public ValType V {
get { return v; }
}
public override string ToString()
{
return string.Format("v.ID = {0}", v.ID);
}
}
class Sample {
static void Main()
{
C c = new C();
Console.WriteLine(c);
// 値型のプロパティに変更を加えようとする
c.V.SetID(3);
// 意図に反して変更が反映されないように見える
Console.WriteLine(c);
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
' IDフィールドの値を設定するメソッド
Public Sub SetID(ByVal newID As Integer)
ID = newID
End Sub
End Structure
Class C
Dim _v As ValType = New ValType()
Public ReadOnly Property V As ValType
Get
Return _v
End Get
End Property
Public Overrides Function ToString() As String
Return String.Format("v.ID = {0}", v.ID)
End Function
End Class
Class Sample
Shared Sub Main()
Dim c As New C()
Console.WriteLine(c)
' 値型のプロパティに変更を加えようとする
c.V.SetID(3)
' 意図に反して変更が反映されないように見える
Console.WriteLine(c)
End Sub
End Class
v.ID = 0 v.ID = 0
using System;
// クラス(参照型)
class RefType {
public int ID;
// IDフィールドの値を設定するメソッド
public void SetID(int newID)
{
ID = newID;
}
}
class C {
RefType r = new RefType();
// 参照型のプロパティ
public RefType R {
get { return r; }
}
public override string ToString()
{
return string.Format("r.ID = {0}", r.ID);
}
}
class Sample {
static void Main()
{
C c = new C();
Console.WriteLine(c);
// 参照型のプロパティに変更を加える
c.R.SetID(3);
// 意図したとおり変更が反映される
Console.WriteLine(c);
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
' IDフィールドの値を設定するメソッド
Public Sub SetID(ByVal newID As Integer)
ID = newID
End Sub
End Class
Class C
Dim _r As RefType = New RefType()
Public ReadOnly Property R As RefType
Get
Return _r
End Get
End Property
Public Overrides Function ToString() As String
Return String.Format("r.ID = {0}", r.ID)
End Function
End Class
Class Sample
Shared Sub Main()
Dim c As New C()
Console.WriteLine(c)
' 参照型のプロパティに変更を加える
c.R.SetID(3)
' 意図したとおり変更が反映される
Console.WriteLine(c)
End Sub
End Class
r.ID = 0 r.ID = 3
この場合も、先の例と同様に一時変数に代入してからメソッド呼び出しを行うことで変更を反映させることができます。
一見すると予想に反する動作であるにも関わらず、このようなコードはコンパイルエラーとはならないため注意する必要があります。 インデクサを多用するListやDictionaryなどのジェネリックコレクションで値型を扱う場合にはこういった問題に遭遇しやすいので注意が必要です。
同値性・同一性の比較
Equalsメソッドを使うと、二つのインスタンスが等しいかどうかの比較が行われます。 Equalsメソッドのデフォルトの動作は値型と参照型で次のように異なります。
- 値型の場合
- 二つの値がビット単位で等しい場合にtrueとなる
- 参照型の場合
- 二つの参照が同一のインスタンスを参照している場合にtrueとなる
つまり、値型では同値性の比較が行われ、参照型では同一性の比較が行われます。
using System;
// 構造体(値型)
struct ValType {
public int ID;
public ValType(int id)
{
ID = id;
}
}
class Sample {
static void Main()
{
ValType a = new ValType(1);
ValType b = a;
ValType c = new ValType(2);
b.ID = 2;
// Equalsメソッドで3つの変数を比較する
Console.WriteLine("a.Equals(b) : {0}", a.Equals(b));
Console.WriteLine("a.Equals(c) : {0}", a.Equals(c));
Console.WriteLine("b.Equals(c) : {0}", b.Equals(c));
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Structure
Class Sample
Shared Sub Main()
Dim a As New ValType(1)
Dim b As ValType = a
Dim c As New ValType(2)
b.ID = 2
' Equalsメソッドで3つの変数を比較する
Console.WriteLine("a.Equals(b) : {0}", a.Equals(b))
Console.WriteLine("a.Equals(c) : {0}", a.Equals(c))
Console.WriteLine("b.Equals(c) : {0}", b.Equals(c))
End Sub
End Class
a.Equals(b) : False a.Equals(c) : False b.Equals(c) : True
using System;
// クラス(参照型)
class RefType {
public int ID;
public RefType(int id)
{
ID = id;
}
}
class Sample {
static void Main()
{
RefType a = new RefType(1);
RefType b = a;
RefType c = new RefType(2);
b.ID = 2;
// Equalsメソッドで3つの変数を比較する
Console.WriteLine("a.Equals(b) : {0}", a.Equals(b));
Console.WriteLine("a.Equals(c) : {0}", a.Equals(c));
Console.WriteLine("b.Equals(c) : {0}", b.Equals(c));
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
End Class
Class Sample
Shared Sub Main()
Dim a As New RefType(1)
Dim b As RefType = a
Dim c As New RefType(2)
b.ID = 2
' Equalsメソッドで3つの変数を比較する
Console.WriteLine("a.Equals(b) : {0}", a.Equals(b))
Console.WriteLine("a.Equals(c) : {0}", a.Equals(c))
Console.WriteLine("b.Equals(c) : {0}", b.Equals(c))
End Sub
End Class
a.Equals(b) : True a.Equals(c) : False b.Equals(c) : False
EqualsメソッドをオーバーライドしたりIEquatable<T>インターフェイスを実装することでEqualsメソッドの動作を変えることができます。 例えば、String.Equalsメソッドが文字列の同値性の比較を行うように、参照型でも同値性の比較を行うように実装することができます。 このほか、任意の型同士で参照の比較(同一性の比較)を行いたい場合は、Object.ReferenceEqualsメソッドを使うことができます。
ボックス化
値型のインスタンスをobject型変数に代入する場合、スタックに配置されている値型インスタンスの複製が作成され、object型変数に箱詰めした上でヒープに配置されます。 これをボックス化(boxing)と呼びます。 ボックス化の際インスタンスの複製が作成されるため、元のインスタンスとボックス化されたインスタンスは別々のものとなります。 そのため、元のインスタンスに変更を加えてもボックス化されたインスタンスには影響しません。
参照型のインスタンスをobject型変数に代入する場合は単にアップキャストとなるだけで、ボックス化は行われません。 参照がobject型変数に代入されるだけとなるため、当然object型に代入されるインスタンスは元のインスタンスと同一のものとなります。
using System;
// 構造体(値型)
struct ValType {
public int ID;
public ValType(int id)
{
ID = id;
}
public override string ToString()
{
return string.Format("ID = {0}", ID);
}
}
class Sample {
static void Main()
{
ValType v = new ValType(1);
object o = v; // ボックス化
Console.WriteLine("v:{0}, o:{1}", v, o);
v.ID = 2;
Console.WriteLine("v:{0}, o:{1}", v, o);
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
Public Overrides Function ToString() As String
Return String.Format("ID = {0}", ID)
End Function
End Structure
Class Sample
Shared Sub Main()
Dim v As New ValType(1)
Dim o As Object = v ' ボックス化
Console.WriteLine("v:{0}, o:{1}", v, o)
v.ID = 2
Console.WriteLine("v:{0}, o:{1}", v, o)
End Sub
End Class
v:ID = 1, o:ID = 1 v:ID = 2, o:ID = 1
using System;
// クラス(参照型)
class RefType {
public int ID;
public RefType(int id)
{
ID = id;
}
public override string ToString()
{
return string.Format("ID = {0}", ID);
}
}
class Sample {
static void Main()
{
RefType r = new RefType(1);
object o = r; // アップキャスト
Console.WriteLine("r:{0}, o:{1}", r, o);
r.ID = 2;
Console.WriteLine("r:{0}, o:{1}", r, o);
}
}
Imports System
' クラス(参照型)
Class RefType
Public ID As Integer
Public Sub New(ByVal id As Integer)
Me.ID = id
End Sub
Public Overrides Function ToString() As String
Return String.Format("ID = {0}", ID)
End Function
End Class
Class Sample
Shared Sub Main()
Dim r As New RefType(1)
Dim o As Object = r ' アップキャスト
Console.WriteLine("r:{0}, o:{1}", r, o)
r.ID = 2
Console.WriteLine("r:{0}, o:{1}", r, o)
End Sub
End Class
r:ID = 1, o:ID = 1 r:ID = 2, o:ID = 2
逆に、object型変数から値型のインスタンスを取り出す際にはボックス化解除(unboxing)が行われます。 ボックス化解除では、object型に箱詰めされている値型インスタンスを取り出して値型変数に代入します。
object型変数への代入だけでなく、値型が実装するインターフェイス型へ代入する場合にもボックス化が行われます。
ボックス化ではインスタンスの複製が作成されるため、参照型のキャストと比べると若干コストのある操作となります。 値型のボックス化・ボックス化解除と参照型のアップキャスト・ダウンキャストの速度を比較すると次のようになります。
using System;
using System.Diagnostics;
// 構造体(値型)
struct ValType {}
class Sample {
static void Main()
{
for (var c = 0; c < 5; c++) {
var v = new ValType();
object o;
var sw = Stopwatch.StartNew();
for (var i = 0; i < 10 * 1000 * 1000; i++) {
o = v; // ボックス化
v = (ValType)o; // ボックス化解除
}
Console.WriteLine(sw.Elapsed);
}
}
}
00:00:00.0153980 00:00:00.0149546 00:00:00.0161484 00:00:00.0174094 00:00:00.0154963
00:00:00.0384817 00:00:00.0356860 00:00:00.0328524 00:00:00.0318040 00:00:00.0316627
using System;
using System.Diagnostics;
// クラス(参照型)
class RefType {}
class Sample {
static void Main()
{
for (var c = 0; c < 5; c++) {
var r = new RefType();
object o;
var sw = Stopwatch.StartNew();
for (var i = 0; i < 10 * 1000 * 1000; i++) {
o = r; // アップキャスト
r = (RefType)o; // ダウンキャスト
}
Console.WriteLine(sw.Elapsed);
}
}
}
00:00:00.0012479 00:00:00.0012820 00:00:00.0012420 00:00:00.0012590 00:00:00.0012509
00:00:00.0027001 00:00:00.0026039 00:00:00.0025005 00:00:00.0025088 00:00:00.0026163
非ジェネリックコレクションで値型を扱う場合、コレクションへの格納・取り出しを行う度にボックス化・ボックス化解除されることになります。 そのため、パフォーマンスの観点からもArrayListなどの非ジェネリックコレクションよりもボックス化・ボックス化解除が発生しないListなどのジェネリックコレクションを使うことが推奨されます。
代入の速度
値型の代入ではインスタンスの複製が行われるため、型のサイズが大きくなるほど代入にかかるコストは大きくなります。 参照型ではインスタンスの複製は行われないため、型のサイズによらず代入にかかるコストは一定となります。
using System;
using System.Diagnostics;
// 構造体(値型)
struct ValType {
public int Field1;
}
class Sample {
static void Main()
{
for (var c = 0; c < 5; c++) {
ValType v1 = new ValType();
ValType v2;
var sw = Stopwatch.StartNew();
for (var i = 0; i < 100 * 1000 * 1000; i++) {
v2 = v1; // 代入
}
Console.WriteLine(sw.Elapsed);
}
}
}
00:00:00.1371791 00:00:00.1295024 00:00:00.1292611 00:00:00.1309864 00:00:00.0770210
00:00:00.0967827 00:00:00.0875368 00:00:00.0853119 00:00:00.0854036 00:00:00.0864029
using System;
using System.Diagnostics;
// クラス(参照型)
class RefType {
public int Field1;
}
class Sample {
static void Main()
{
for (var c = 0; c < 5; c++) {
RefType r1 = new RefType();
RefType r2;
var sw = Stopwatch.StartNew();
for (var i = 0; i < 100 * 1000 * 1000; i++) {
r2 = r1; // 代入
}
Console.WriteLine(sw.Elapsed);
}
}
}
00:00:00.1231474 00:00:00.1206678 00:00:00.0887434 00:00:00.0872789 00:00:00.0861327
00:00:00.1159542 00:00:00.0857808 00:00:00.0854572 00:00:00.0859070 00:00:00.0863372
using System;
using System.Diagnostics;
// 構造体(値型)
struct ValType {
public int Field1;
public int Field2;
public int Field3;
public int Field4;
public int Field5;
public int Field6;
public int Field7;
public int Field8;
}
class Sample {
static void Main()
{
for (var c = 0; c < 5; c++) {
ValType v1 = new ValType();
ValType v2;
var sw = Stopwatch.StartNew();
for (var i = 0; i < 100 * 1000 * 1000; i++) {
v2 = v1; // 代入
}
Console.WriteLine(sw.Elapsed);
}
}
}
00:00:00.1145360 00:00:00.1139195 00:00:00.0907852 00:00:00.0858125 00:00:00.0871278
00:00:00.2255986 00:00:00.1306481 00:00:00.1284407 00:00:00.1298623 00:00:00.1288758
using System;
using System.Diagnostics;
// クラス(参照型)
class RefType {
public int Field1;
public int Field2;
public int Field3;
public int Field4;
public int Field5;
public int Field6;
public int Field7;
public int Field8;
}
class Sample {
static void Main()
{
for (var c = 0; c < 5; c++) {
RefType r1 = new RefType();
RefType r2;
var sw = Stopwatch.StartNew();
for (var i = 0; i < 100 * 1000 * 1000; i++) {
r2 = r1; // 代入
}
Console.WriteLine(sw.Elapsed);
}
}
}
00:00:00.1113680 00:00:00.1196529 00:00:00.0888160 00:00:00.0872582 00:00:00.0874921
00:00:00.1106691 00:00:00.0856765 00:00:00.0905668 00:00:00.0860375 00:00:00.0907923
サイズの大きい構造体の代入を多数行う必要がある場合、クラスに置き換えることを検討することでコストを下げることができます。 クラスまたは構造体の選択(MSDN)では、構造体とクラスのどちらを選択するかという基準の1つに「サイズが16バイト未満かどうか」というガイドラインが設定されています。
インスタンスの複製 (Object.MemberwiseClone)
Object.MemberwiseCloneメソッドを使ってインスタンスの複製を作成する際、フィールドが値型か参照型かによって複製時の動作が異なります。
値型のフィールドはビット単位での複製(詳細コピー)が行われるのに対し、参照型のフィールドは参照のみが複製されます(簡易コピー)。 そのため、MemberwiseCloneメソッドによるインスタンス複製後の参照型フィールドは、複製元の同一フィールドと同じインスタンスを参照することになります。
using System;
// 構造体(値型)
struct ValType {
public int ID;
}
// クラス(参照型)
class RefType {
public int ID;
}
class C {
// 値型フィールド
public ValType V = new ValType();
// 参照型フィールド
public RefType R = new RefType();
// インスタンスの複製を作成するメソッド
public C Clone()
{
return (C)MemberwiseClone();
}
}
class Sample {
static void Main()
{
C c1 = new C();
Console.WriteLine("c1.V.ID = {0}, c1.R.ID = {1}", c1.V.ID, c1.R.ID);
// インスタンスを複製する
C c2 = c1.Clone();
// 複製後のインスタンスのフィールドに変更を加える
c2.V.ID = 2;
c2.R.ID = 2;
Console.WriteLine("c1.V.ID = {0}, c1.R.ID = {1}", c1.V.ID, c1.R.ID);
Console.WriteLine("c2.V.ID = {0}, c2.R.ID = {1}", c2.V.ID, c2.R.ID);
Console.WriteLine("Object.ReferenceEquals(c1.R, c2.R) = {0}", Object.ReferenceEquals(c1.R, c2.R));
}
}
Imports System
' 構造体(値型)
Structure ValType
Public ID As Integer
End Structure
' クラス(参照型)
Class RefType
Public ID As Integer
End Class
Class C
' 値型フィールド
Public V As ValType = New ValType()
' 参照型フィールド
Public R As RefType = New RefType()
' インスタンスの複製を作成するメソッド
Public Function Clone() As C
Return DirectCast(MemberwiseClone(), C)
End Function
End Class
Class Sample
Shared Sub Main()
Dim c1 As New C()
Console.WriteLine("c1.V.ID = {0}, c1.R.ID = {1}", c1.V.ID, c1.R.ID)
' インスタンスを複製する
Dim c2 As C = c1.Clone()
' 複製後のインスタンスのフィールドに変更を加える
c2.V.ID = 2
c2.R.ID = 2
Console.WriteLine("c1.V.ID = {0}, c1.R.ID = {1}", c1.V.ID, c1.R.ID)
Console.WriteLine("c2.V.ID = {0}, c2.R.ID = {1}", c2.V.ID, c2.R.ID)
Console.WriteLine("Object.ReferenceEquals(c1.R, c2.R) = {0}", Object.ReferenceEquals(c1.R, c2.R))
End Sub
End Class
c1.V.ID = 0, c1.R.ID = 0 c1.V.ID = 0, c1.R.ID = 2 c2.V.ID = 2, c2.R.ID = 2 Object.ReferenceEquals(c1.R, c2.R) = True
インスタンスの複製とMemberwiseCloneメソッド、詳細コピーと簡易コピーの動作についてはオブジェクトの複製 §.Object.MemberwiseCloneによる複製でも解説しています。
デフォルト値
初期値を指定しないでフィールドやローカル変数の宣言を行った場合、デフォルト値で初期化されます。 値型のデフォルト値は0
(もしくは0
に相当する値)で、参照型のデフォルト値はヌル参照(null/Nothing)です。
型のデフォルト値について詳しくは型の種類・サイズ・精度・値域 §.型のデフォルト値を参照してください。
値型か参照型か調べる
実行時に型が値型か参照型かを調べるには、型情報(Type)を取得しIsValueTypeプロパティを参照します。
using System;
// 構造体(値型)
struct ValType {}
// クラス(参照型)
class RefType {}
class Sample {
static void PrintType(Type t)
{
Console.WriteLine("{0,-20}: {1}", t.FullName, t.IsValueType ? "値型" : "参照型");
}
static void Main()
{
PrintType(typeof(int));
PrintType(typeof(string));
PrintType(typeof(object));
PrintType(typeof(ValType)); // 構造体
PrintType(typeof(RefType)); // クラス
PrintType(typeof(int[])); // 配列
PrintType(typeof(DayOfWeek)); // 列挙型
PrintType(typeof(IDisposable)); // インターフェイス型
PrintType(typeof(EventHandler)); // デリゲート型
}
}
Imports System
' 構造体(値型)
Structure ValType
End Structure
' クラス(参照型)
Class RefType
End Class
Class Sample
Shared Sub PrintType(ByVal t As Type)
Console.WriteLine("{0,-20}: {1}", t.FullName, If(t.IsValueType, "値型", "参照型"))
End Sub
Shared Sub Main()
PrintType(GetType(Integer))
PrintType(GetType(String))
PrintType(GetType(Object))
PrintType(GetType(ValType)) ' 構造体
PrintType(GetType(RefType)) ' クラス
PrintType(GetType(Integer())) ' 配列
PrintType(GetType(DayOfWeek)) ' 列挙型
PrintType(GetType(IDisposable)) ' インターフェイス型
PrintType(GetType(EventHandler)) ' デリゲート型
End Sub
End Class
System.Int32 : 値型 System.String : 参照型 System.Object : 参照型 ValType : 値型 RefType : 参照型 System.Int32[] : 参照型 System.DayOfWeek : 値型 System.IDisposable : 参照型 System.EventHandler : 参照型
型情報の取得について、およびTypeクラスについて詳しくはリフレクションで解説しています。
ジェネリック型の制約
ジェネリック型を定義する際、型パラメータに制約(constraints)を持たせることができます。 型パラメータに指定できる型を特定の基本型やインターフェイスを実装する型に限定するのと同様に、値型・参照型のみに限定させることもできます。
C#ではwhere句を使ってwhere T : struct
とすれば型パラメータT
を値型に、where T : class
とすれば参照型に限定することができます。 VBではOf句を使ってOf T As Structure
とすればT
を値型に、Of T As Class
とすれば参照型に限定できます。
using System;
using System.Collections.ObjectModel;
// 型パラメータTを値型のみに限定したコレクション
class ValTypeCollection<T> : Collection<T> where T : struct {
}
// 型パラメータTを参照型のみに限定したコレクション
class RefTypeCollection<T> : Collection<T> where T : class {
}
class Sample {
static void Main()
{
// intは値型なので型を構築できる
ValTypeCollection<int> intcol;
// stringは参照型なので制約と一致せず、型を構築できない
// (コンパイルエラーとなる)
ValTypeCollection<string> strcol;
// error CS0453: 型 'string' は、ジェネリック型のパラメーター 'T'、またはメソッド 'ValTypeCollection<T>' として使用するために、Null非許容の値型でなければなりません
// floatは値型なので制約と一致せず、型を構築できない
// (コンパイルエラーとなる)
RefTypeCollection<float> fltcol;
// error CS0452: 型 'float' は、ジェネリック型のパラメーター 'T'、またはメソッド 'RefTypeCollection<T>' として使用するために、参照型でなければなりません
}
}
Imports System
Imports System.Collections.ObjectModel
' 型パラメータTを値型のみに限定したコレクション
Class ValTypeCollection(Of T As Structure)
Inherits Collection(Of T)
End Class
' 型パラメータTを参照型のみに限定したコレクション
Class RefTypeCollection(Of T As Class)
Inherits Collection(Of T)
End Class
Class Sample
Shared Sub Main()
' Integerは値型なので型を構築できる
Dim intcol As ValTypeCollection(Of Integer)
' Stringは参照型なので制約と一致せず、型を構築できない
' (コンパイルエラーとなる)
Dim strcol As ValTypeCollection(Of String)
' error BC32105: 型引数 'String' は型パラメーター 'T' の 'Structure' 制約を満たしていません。
' Singleは値型なので制約と一致せず、型を構築できない
' (コンパイルエラーとなる)
Dim sngcol As RefTypeCollection(Of Single)
' error BC32106: 型引数 'Single' は型パラメーター 'T' の 'Class' 制約を満たしていません。
End Sub
End Class