構造体

クラスと構造体

クラスとよく似た機能を持つものに構造体というものがあります。


//構造体の定義
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を受け付けません。

引数なしコンストラクターは定義できない

構造体のコンストラクターは必ず引数を指定しなければなりません。
ただし引数付きコンストラクターを定義しても、インスタンスの生成は引数なしでも可能です。


struct MyStruct
{
    public int Num;

    public MyStruct(int num)
    {
        Num = num;
    }

    //引数なしコンストラクターはダメ
    //public MyStruct() { }
}


static void Main(string[] args)
{
    //どちらも可能
    MyStruct ms1 = new MyStruct();
    MyStruct ms2 = new MyStruct(5);
}

引数なしコンストラクターは構造体のフィールドを規定値で初期化します。
この動作を変更することはできません。

コンストラクター内ですべてのフィールドの初期化が必須

クラスのコンストラクターではフィールドの初期化は必須ではなく、初期化しない場合は規定値で自動的に初期化されますが、構造体のコンストラクターではすべてのフィールドを初期化する必要があります。


struct MyStruct
{
    public int Num1;
    public int Num2;
    public string Str;

    public MyStruct(int num)
    {
        Num1 = num;
        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にアクセスするたびに構造体のコピーが渡されるので、書き換えても無意味です。
(書き換えても次にその値にアクセスする手段がない。フィールドは変更されていない)

このような場合は構造体をローカル変数として宣言し、値を書き換え、そのインスタンス自身を代入します。


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 = new MyStruct();
    ms.Num = 5;
    mc.MS = ms;
}

上記の間違いコードは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();
    Console.WriteLine(mc.MS.Num);

    mc.MS.Set(5);
    Console.WriteLine(mc.MS.Num);

    Console.ReadLine();
}
0
0

これも結局はプロパティMSが返すのはインスタンスのコピーだからです。
これがクラスならばインスタンス自身が返されるので見た目通りの動作になります。
(逆にいえば、外部から値を書き換えられてしまうとも言えます)