構造体
クラスと構造体
クラスとよく似た機能を持つものに構造体というものがあります。
//構造体の定義
struct MyPoint
{
public float X;
public float Y;
public MyPoint(float x, float y)
{
X = x;
Y = y;
}
}
static void Main(string[] args)
{
MyPoint p = new MyPoint(5, 7);
Console.WriteLine("X={0}, Y={1}", p.X, p.Y);
Console.ReadLine();
}
X=5, Y=7
構造体はstruct
というキーワードで定義します。
(structure=構造)
構造体の内部にはフィールド、メソッド、プロパティ、コンストラクターなど、クラスとほとんど同じものを持つことができます。
構造体を使用する方法もクラスとほぼ同じで、new
キーワードでコンストラクターを呼び出すことができます。
クラスとの基本的な違いは以下のようになります。
構造体は値型、クラスは参照型
最も大きな違いは構造体は値型、クラスは参照型と言う点です。
値型、参照型については値型と参照型を参照してください。
//構造体
struct MyStruct
{
public int Num;
public MyStruct(int num)
{
Num = num;
}
}
//クラス
class MyClass
{
public int Num;
public MyClass(int num)
{
Num = num;
}
}
//構造体用のメソッド
static void MS(MyStruct ms)
{
ms.Num *= 2;
}
//クラス用のメソッド
static void MC(MyClass mc)
{
mc.Num *= 2;
}
static void Main(string[] args)
{
MyStruct ms = new MyStruct(1);
MyClass mc = new MyClass(1);
MS(ms);
MC(mc);
Console.WriteLine("MyStruct: {0}", ms.Num);
Console.WriteLine("MyClass: {0}", mc.Num);
Console.ReadLine();
}
MyStruct: 1 MyClass: 2
構造体とクラスの二つの定義があるのでちょっと長いですが、中身はどちらも全く同じものです。
MS
メソッドとMC
メソッドはそれぞれのフィールドの値を単純に2倍にしているだけです。
構造体は値型なので、インスタンスをメソッドの引数に渡すとコピーが渡されます。
メソッド内でフィールドを書き換えても、呼び出し元のインスタンスの値には影響しません。
クラスは参照型なので、インスタンスをメソッドの引数に渡すとアドレスが渡されます。
メソッド内ではアドレスを通じて呼び出し元のインスタンスにアクセスします。
そのためフィールドを書き換えると呼び出し元のインスタンスの値に影響します。
この違いは非常に重要なので覚えておいてください。
同じ理由で、クラスはインスタンスをnull
の状態にできますが、構造体のインスタンスはnullを受け付けません。
引数なしのコンストラクター
構造体のコンストラクターは必ず引数を指定しなければなりません。
(C#9.0まで)
ただし引数付きコンストラクターを定義しても、インスタンスの生成は引数なしでも可能です。
class Program
{
struct MyStruct
{
public int Num;
public MyStruct(int num)
{
Num = num;
}
//引数なしコンストラクターは定義できない
//public MyStruct() { }
}
//Programクラスのフィールド
static MyStruct _ms;
static void Main(string[] args)
{
//どれもOK
MyStruct ms1;
MyStruct ms2 = new MyStruct();
MyStruct ms3 = new MyStruct(5);
MyStruct ms4 = default(MyStruct);
//Console.WriteLine(ms1.Num); //初期化前に使用できない
Console.WriteLine(ms2.Num); //0
Console.WriteLine(ms3.Num); //5
Console.WriteLine(ms4.Num); //0
//クラスのフィールドは自動的に初期化されるので使用可能
Console.WriteLine(_ms.Num); //0
}
}
引数なしコンストラクターの場合、フィールドは既定値で初期化されます。
構造体は値型であるため、実はnew
によるコンストラクタの呼び出しも必須ではありません。
ただしint型変数などと同じように、ローカル変数として構造体変数を宣言した場合は値の初期化はされないので、値を代入するまでは使用できません。
クラスのフィールドとして構造体変数を持つ場合は自動的に既定値で初期化されるため、new
を使用することなく構造体を使用することができます。
(クラス変数の場合は既定値がnullなので使用できない)
引数なしコンストラクターの定義(C#10以降)
C#10.0以降、構造体でも引数なしのコンストラクターを定義できるようになりました。
class Program
{
struct MyStruct
{
public int Num;
public MyStruct(int num)
{
Num = num;
}
//C#10.0以降
//引数なしコンストラクターも定義可能
public MyStruct() { Num = 1; }
}
//Programクラスのフィールド
static MyStruct _ms;
static void Main(string[] args)
{
//どれもOK
MyStruct ms1;
MyStruct ms2 = new MyStruct();
MyStruct ms3 = new MyStruct(5);
MyStruct ms4 = default(MyStruct);
//Console.WriteLine(ms1.Num); //初期化前に使用できない
Console.WriteLine(ms2.Num); //1
Console.WriteLine(ms3.Num); //5
Console.WriteLine(ms4.Num); //0
//クラスのフィールドは自動的に初期化されるので使用可能
Console.WriteLine(_ms.Num); //0
}
}
ちなみにdefault演算子はあくまでも既定値を返すだけで、デフォルトコンストラクタは呼び出しません。
つまりC#9.0まではnew Struct()
とdefault(Struct)
は同じ動作でしたが、C#10.0以降では動作が異なります。
コンストラクター内ですべてのフィールドの初期化
クラスのコンストラクターではフィールドの初期化は必須ではなく、初期化しない場合は既定値で自動的に初期化されますが、構造体のコンストラクターではすべてのフィールドを初期化する必要があります。
struct MyStruct
{
public int Num1;
public int Num2;
public string Str;
public MyStruct(int num)
{
Num1 = num;
Num2 = 0;
Str = null;
}
}
コンストラクター内でのフィールドの初期化は不要(C#11以降)
C#11以降、コンストラクター内でのすべてのフィールドの初期化は必須ではなくなりました。
初期化しないフィールドは既定値で初期化されます。
struct MyStruct
{
public int Num1;
public int Num2;
public string Str;
public MyStruct(int num)
{
Num1 = num;
//C#11より
//すべてのフィールドの初期化は必須ではなくなった
//Num2 = 0;
//Str = null;
}
}
ファイナライザー(デストラクター)がない
構造体はファイナライザーを使用できません。
ファイナライザーについてはオブジェクトの破棄の項で説明します。
フィールドを初期化子で初期化できない
構造体のフィールドに初期化子は使用できません。
コンストラクター内で初期化する必要があります。
struct MyStruct
{
//ダメ
public int Num = 10;
}
継承ができない
構造体は継承に関する機能はありません。
継承についてはまだ詳しく説明していないので詳細は省きます。
継承の機能がないので、アクセス修飾子の中で継承に関するものは使用できません。
(protected
、protected internal
)
クラスと構造体の使い分け
構造体はクラスの機能制限版と言った感じになっています。
構造体にできてクラスにできないことは基本的にありません。
そのため、基本的にはクラスを使用します。
構造体は値型なので、値型の特性が有効な場面では構造体のほうが良いかもしれません。
構造体のサイズがおおむね16バイト未満であるならば構造体のほうが高速である(可能性がある)とされます。
これは値型と参照型とではメモリの管理方法が異なるからです。
メモリには「高速だが容量が少ない」スタックという領域と、「低速だが大容量」なヒープという領域があります。
(低速といってもメモリ上のことなのでHDDやSSDなどに比べれば十分高速です)
ローカル変数やメソッドの実引数はスタック領域にデータが保存されます。
値型の場合はデータのすべてがスタック領域に保存されますが、参照型の場合はヒープに実データを保存し、スタックには実データへのアドレスが保存されます。
構造体は値型なのでスタック領域に保存されます。
参照型であるクラスに比べて高速にデータをやり取りできます。
しかし構造体のサイズが大きくなるとコピーのコストも無視できなくなります。
その境目がおよそ16バイトとされているようです。
構造体の注意点
値型と参照型の違いを意識しないと意図した通りの動作にならないことがあるので注意してください。
上のサンプルコードで例示したような、引数にインスタンスを渡すときの動作の違いもそのひとつです。
クラスなどのフィールドに構造体を持つ場合、少しややこしくなります。
例えば以下のようなコードは許されていません。
struct MyStruct
{
public int Num;
}
class MyClass
{
private MyStruct ms = new MyStruct();
public MyStruct MS { get { return ms; } }
}
static void Main(string[] args)
{
MyClass mc = new MyClass();
Console.WriteLine(mc.MS.Num);
//エラー
mc.MS.Num = 5;
}
MyClass
のプロパティMS
は、構造体のインスタンスms
を返します。
それを利用してMyClass
のインスタンスmc
からms
にアクセスし、ms
内のフィールドNum
を書き換えようとしています。
文法的に問題なさそうですが、しかしこれはできません。
MyClass
のプロパティMS
が返すのは、フィールドms
自身ではなくそのコピーだからです。
プロパティMS
にアクセスするたびに構造体のコピーが渡されるので、その中身を書き換えてもコピー元には影響しません。
コピーされたデータはどこにも保存していないので、次にそのデータにアクセスする手段がなく無意味な処理になります。
C#では、このような無意味なコードがコンパイル時に検出された場合はエラーになります。
コンパイルエラーを回避するには構造体を変数で受け取り、値を書き換え、それをコピー元に代入します。
struct MyStruct
{
public int Num;
}
class MyClass
{
private MyStruct ms = new MyStruct();
public MyStruct MS {
get { return ms; }
set { ms = value; }
}
}
static void Main(string[] args)
{
MyClass mc = new MyClass();
MyStruct ms = mc.MS;
ms.Num = 5;
mc.MS = ms; //mc内のMyStructインスタンスを上書き
Console.WriteLine(mc.MS.Num);
}
5
上記の間違いコードはVisual Studioがコンパイル時エラーにしてくれるので発見は容易です。
以下の場合はエラーにはならず、値が意図した通りにならないので注意が必要です。
struct MyStruct
{
public int Num { get; private set; }
public void Set(int n)
{
Num = n;
}
}
class MyClass
{
private MyStruct ms = new MyStruct();
public MyStruct MS { get { return ms; } }
}
static void Main(string[] args)
{
MyClass mc = new MyClass();
//MSのコピーを取得し、Numにアクセスしている
Console.WriteLine(mc.MS.Num);
//MSのコピーを取得し、Setメソッドを実行している
//このSetメソッドで書き換わるのはコピーされた構造体の値であり
//コピー元には影響しない
mc.MS.Set(5);
Console.WriteLine(mc.MS.Num);
Console.ReadLine();
}
0 0
これも結局はプロパティMS
が返すのは構造体インスタンスのコピーだからです。
これがクラスならばインスタンス自身が返されるので見た目通りの動作になります。
(逆にいえば、外部から値を書き換えられてしまうとも言えます)