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) newでインスタンス化すればメンバーの初期値は両方とも同じ
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)は継承できないことが関係して、継承に関わる宣言がstructではできません。また、アクセス修飾子を省略したときの既定の値は双方ともに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:すぐリンク切れになるのでリンク切れのしてたらタイトルで検索してください。