C#のクラスと構造体の違い・使い分け方

C#でクラス(class)と構造体(struct)の違いは何か?それぞれどのような性質があるのか?また、使い分け方の方針の紹介です。基本的にほとんど同じですが割と性質が違います。

性質の違いを考慮しながらどちらを使用するかを検討することになります。…と、言っても十中八九クラスしか選ばないので常にクラスを選択して、「値渡し」の性質が必要であれば構造体の採用を考慮するという感じになります。

以下に性質の違いをまとめています。

宣言時の違い

クラスや構造体を宣言する時の違いです。

# 項目 クラス(class) 構造体(struct)
1 abstruct宣言 できる できない
2 sealed宣言 できる できない
3 具象クラスの継承 できる できない
4 interfaceの継承 できる できる
5 abstructの継承 できる できない
6 メンバーの初期値 既定値で初期化 既定値で初期化、もしくは自分で明示
7 フィールドの初期化 宣言と同時に可能 できない
8 コンストラクタの引数 なくてもOK 必ず何か指定する必要あり

構造体(struct)は継承できないため、継承関係の宣言ができません。例外的にinterfaceの継承だけは出来ます。

(7), (8) は構造体は、で暗黙のデフォルトコンストラクターで全部既定値で初期化するか、明示的なコンストラクターには引数が必要 & メンバーの初期値を全部自分で記述する必要があります。

以下、長いですがそれぞれのコード例です。

// (1) 構造体は抽象クラス宣言できない
public abstract struct AbstStruct { ... } 
// > CS0106 修飾子 'abstract' がこの項目に対して有効ではありません

// (2) 構造体は継承禁止を指定できない
public sealed struct SampleStruct { ... }
// > CS0106 修飾子 'sealed' がこの項目に対して有効ではありません。    

// (3) 構造体は具象クラスを継承できない。
public struct BaseStruct { ... }
public class BaseClass { ... }
public struct DerivedStruct_1 : BaseStruct // エラー
public struct DerivedStruct_2 : BaseClass  // エラー
// > CS0527 インターフェイス リストの型 'BaseStruct|BaseClass' はインターフェイスではありません。 

// 構造体はインターフェースは継承できる
public interface ISample { ... }
public struct SampleStruct : ISample // これはOK

// (5) 構造体は抽象クラスを継承できない。
public abstract class AbstClass { ... }
public struct DerivedStruct : AbstClass // エラー
// > CS0527 インターフェイス リストの型 'AbstClass' はインターフェイスではありません。    

// (6) メンバーの初期値は両方とも同じ
public struct StructSample
{
    public int I;    // 0
    public byte B;   // 0
    public double D; // 0.0
    public string S;         // null
    public StringBuilder Sb; // null
}

public class ClassSample
{
    public int I;    // 0
    public byte B;   // 0
    public double D; // 0.0
    public string S;         // null
    public StringBuilder Sb; // null
}

// (7) 構造体はメンバーの初期化がインラインではできない
public struct StructSample
{
    public string Str;
    public StringBuilder Sb = new StringBuilder("asdf"); // エラー
    // > CS0573 'StructSample': 構造体にインスタンス プロパティまたはフィールド初期化子を含めることはできません。
}

// (8) 構造体の明示的なコンストラクタには引数が必ず必要 
public struct StructSample
{
    public string Str;
    public StringBuilder Sb;

    public StructSample() { } // エラー
    // > CS0568 構造体に明示的なパラメーターのないコンストラクターを含めることはできません。

    public StructSample(string str) { this.Str = str } // エラー
    // > CS0171 フィールド 'StructSample.Sb' は、
    // コントロールが呼び出し元に返される前に割り当てられている必要があります。

    // こうしないとダメ
    public StructSample(string str) // OK
    {
        this.Str = str;
        this.Sb = new StringBuilder(str); // 全メンバーの初期値をコンストラクタで指定
    }
}

メソッド記述時の違い

構造体にもメソッドが書けます。クラスを同じように処理を記述できますがその時の違いです。

# 項目 クラス(class) 構造体(struct)
1 publicメソッド宣言 できる できる
2 privateメソッド宣言 できる できる
3 internalメソッド宣言 できる できる
4 protectedメソッド宣言 できる できない
5 protected internalメソッド宣言 できる できない
6 アクセス修飾子省略時のアクセスレベル private private

こちらも構造体(struct)は継承できないことが関係して、継承関係の宣言ができません。また、C++と違って既定のアクセスレベルは共通でprivate固定です。

使用するときの性質

# 項目 クラス(class) 構造体(struct)
1 インスタンス生成方法 newで生成 newで生成, ユーザーコードで初期化
2 引数の渡され方 参照渡し 値渡し
3 メモリレイアウト 不定 宣言順に並ぶ
4 データ位置 ヒープ スタック
5 nullの利用 できる できない

上記の(2)と(5)が最も重要な性質の違いです。メソッド等の引数で構造体を渡すと、値渡し、すなわちコピーが作成されて呼び出し先に渡されます。従ってメソッド内で変更したとしても呼び出し元に変更が反映されまん。また、構造体は基本的にnullにできません。必ず何か値があることが期待できます。(ただし、構造体は"null許容型"としてnullが代入できる形式もあるのであくまで「基本的には」となります。)

引数に渡すとクラスは参照渡しになるのに対して、構造体は値渡しになります。但し、構造体内のクラスメンバー(class)は変更が反映されます。全部構造体であれば変更は反映されません。

(3) ですが、クラスでint, longの順にフィールドを宣言した場合メモリ上の配置が同じ順に並ぶとは限りませんが、構造体の場合、おおよそ1~4バイト目がint, 5~12バイト目がlongのように順番にメモリ上に配置されます。

詳しくは++C++ 複合型のレイアウトを参照ください。

(4) ですが、ヒープに取るとGCが動くまではそのままですが、スタックに取られている場合スコープを抜ければオブジェクトが解放されるのでその点で何か有利になる事があるかもしれません。

以下にコード例です。

// (1) 両方ともnewを使ってインスタンスを生成する
var s = new StructSample();
var c = new ClassSample();

// 構造体はnewしなくても宣言後に初期化すれば使用できる
StructSample ss;
ss.D = 5;
Console.WriteLine(ss.D);

// ss.D = 5; を書かないと以下のように怒られる。
// > CS0170 フィールド 'D' は、割り当てられていない可能性があります。

// (2) 構造体は参照渡しになる
public struct StructSample // 使用するクラス
{
    public string Str;
    public StringBuilder Sb;

    public StructSample(string str)
    {
        this.Str = str;
        this.Sb = new StringBuilder(str);
    }
}

public static void Main(string[] args)
{
    var s = new StructSample("asdf");
    Console.WriteLine($"{s.Str}, {s.Sb}");
    // > asdf, asdf

    Foo(s);
    Console.WriteLine($"{s.Str}, {s.Sb}"); // ★★★値渡しなので変更が反映されない
    // > asdf, asdf1234

    // メソッド内で値を変えたい時はrefキーワードを使用して渡す
    Foo2(ref s);
    Console.WriteLine($"{s.Str}, {s.Sb}");
    // > zzzz, asdf12341234
}

public static void Foo(StructSample s) // 構造体を引数にとって値を変更
{
    s.Str = "xxxx";
    s.Sb.Append("1234");
}

public static void Foo2(ref StructSample s) // ref渡し版
{
    s.Str = "zzzz";
    s.Sb.Append("1234");
}

// (5) null
StructSample ss1 = null; // エラー、構造体はnullにできない
StructSample? ss2 = null; // 但し?を付けてnull許容型にすれば特別に指定できる

new ClassSample cs = null; // クラスは普通にnullにできる

.NETだとintはSystem.Int32、doubleはSystem.Doubleという構造体の別名宣言となっているため、上記の性質を上手に使用しています。冒頭でも述べましたが同じような挙動が必要であれば構造体を選ぶという感じかと思います。

どちらを使用するべきか?

これまで、性質の違いを見てきましたが、どういうときに構造体を使うのかは、MSDNに「クラスまたは構造体の選択」というタイトルのページがあり*1、詳細な使い分けの方針が書かれています。

抜粋すると大凡以下のようになります。

  • (1) 有効期間が短いこと
  • (2) プリミティブ型のような1つの値を論理的に表す (int、doubleなど)
  • (3) 16バイト未満のサイズ
  • (4) 変更できない
  • (5) 頻繁にボックス化しない(=objectにキャストしない)

1つのメソッド内で16バイト以下の変更できない型でobject型にキャストしないことって、条件がかなり厳しいです。実際、そのようなケースはほぼありませんが、小さいサイズの構造体を狭い範囲で使えばいいことがあるかもしれませんよって感じだと思います。

以上です。

*1:すぐリンク切れになるのでタイトルで検索してください。