Menu

C#の構造体:値型、構造体とクラスの違い、使うべき場面

C#の構造体の仕組みを解説します。値のセマンティクスと代入時のコピー、List<T>の変更エラー、コンストラクターのルール、等価性、ボックス化、readonly structとrecord struct、そして構造体が適している場面の指針とともに構造体とクラスを比較します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

構造体は自分で定義する値型です。フィールド、プロパティ、メソッド、コンストラクターを持つクラスのように見えますが、構造体型の変数はオブジェクトへの参照ではなく、データを直接含んでいます。この1つの違いによって、代入、メソッドの呼び出し、等価性、null の振る舞いが変わります。

値のセマンティクス:代入でコピーされる

組み込みの数値型、bool、char、DateTime、TimeSpan、Guid はすべて構造体です。自分で作るものも同じように動きます。

出力:

struct: s1.X = 1, s2.X = 50
class:  c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150

構造体では s2 は独立したコピーで、Move も自分のコピーに対して動作します。メソッドから呼び出し側の構造体を変更させるには、ref で参照渡しします(refとoutを参照)。変更を許さずに大きな構造体のコピーを避けるために、C# 7.2では in 引数が追加されました。

値型であることのその他の結果:

  • 構造体の変数は決して null になれません。PointS p = null; はコンパイルできません。「値がない」ことに意味があるなら PointS?(null許容値型)を使います。
  • 初期化されていない構造体はすべてゼロです。数値のフィールドは 0、bool のフィールドは false、参照のフィールドは null です。
  • 構造体は別の構造体やクラスを継承できず、何も構造体を継承できません。インターフェースは実装できます。
  • ローカルの構造体の変数は通常スタックか、それを含むオブジェクトの中にあるので、作ってもヒープの割り当ては発生しません。

構造体のListの落とし穴:CS1612

構造体を読むとコピーが得られるので、一見無害なこの行はコンパイルできません。

var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable

List<T> のインデクサーは、要素のコピーを返すメソッドです。その一時的なコピーの X を設定しても何も変わらないので、コンパイラが止めます。プロパティが構造体を返す場合にも同じエラーが出ます:order.Location.X = 10。コピーして、変更して、書き戻します。

出力:

35
7

配列は例外です。array[0] はコピーではなく要素そのものです。繰り返し起きるこの面倒さが、構造体を不変にせよという標準的な助言の理由です。構造体を変更できなければ、コピーを変更して失うものもありません。

コンストラクターと既定値

C# 7から9までは、構造体のコンストラクターのルールは厳格です。

  • 引数なしのコンストラクターは宣言できません。new PointS() は常に存在し、すべてのフィールドをゼロにします。
  • 宣言したコンストラクターは、戻る前にすべてのフィールド(と自動実装プロパティ)に代入しなければなりません。
  • インスタンスフィールドにフィールド初期化子(public int X = 1;)は使えません。

出力:

19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)

Money は、既定値が重要な理由も示しています。ゼロにされた Money の通貨は null で、配列、default(T)、初期化されていないフィールドのどれもがそれを生み出すので、コードはそれを扱わなければなりません。

新しいバージョンでは、これらのルールが緩和されました。

// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
    public int Retries = 3;
    public Settings() { }
}

// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.

C# 10の引数なしのコンストラクターは new Settings() では実行されますが、default(Settings) や配列の要素では実行されず、それらは今もすべてゼロです。この違いは人を驚かせるので、注意して使います。

readonly struct(C# 7.2)

構造体そのものに readonly を付けると、コンパイラが不変性を強制します。すべてのフィールドは readonly、すべての自動実装プロパティは読み取り専用でなければなりません。

public readonly struct Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius) { Celsius = celsius; }

    public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta);   // returns a new value
}

意図を文書化するだけでなく、性能にも役立ちます。readonlyでない構造体を readonly のフィールドに格納したり in 引数として渡したりすると、コンパイラはメソッドを呼ぶたびにそれをコピーします(メソッドがそれを変更しないことを知りえないからです)。readonly struct なら、そうした防御的なコピーは不要です。C# 7.0でも、Money が読み取り専用のプロパティで行っているように、すべてのフィールドを readonly にできます。それで不変性は得られますが、コンパイラが信頼するのは readonly と宣言された構造体だけなので、コピーの節約は得られません。

等価性

構造体に対する Equals は既定でフィールドごとに比較し、これは望ましい値の振る舞いです。ただし既定の実装(ValueType.Equals)はリフレクションを使うことがあって遅く、== 演算子はまったく定義されていません。自分で作った構造体に対する a == b はエラーCS0019です。構造体を比較するなら、両方を実装します。

出力:

True
True
True

IEquatable<T> の実装はコレクションにとって重要です。HashSet<T>、Dictionary<TKey, TValue>、List<T>.Contains は、各値をボックス化して Equals(object) を呼ぶ代わりに、Equals(GridCell) を直接呼びます。レコード構造体(後述)はこれらすべてを生成してくれます。

ボックス化

構造体を object やインターフェースの型に変換するとボックス化されます。ランタイムが値を新しいヒープのオブジェクトにコピーします。その後、ボックスと元の値は独立しています。

出力:

2
0

ボックス化は毎回割り当てのコストがかかります。だから ArrayList のような古いジェネリックでないコレクションは値型で遅く、ジェネリックがそれらに取って代わりました。また、インターフェースを通してアクセスした変更可能な構造体は、元の値ではなくボックスの中で変更されることになり、構造体を不変に保つべき理由がもう1つ増えます。

構造体とクラスの比較

構造体クラス
種類値型参照型
代入と引数データをコピー参照をコピー
null になれるかなれない(T? はなれる)なれる
既定値すべてのフィールドがゼロnull
継承なし。インターフェースは実装できる1つの基底クラス、インターフェース
==オーバーロードしない限り定義されないオーバーロードしない限り参照の等価性
Equals の既定フィールドを比較参照を比較
割り当てインライン(スタックまたはそれを含むオブジェクト)ヒープ、ガベージコレクション
向いているもの小さな不変の値エンティティ、大きな、または共有される状態

構造体を使うべきとき

次のすべてが成り立つときに構造体を選びます。型が1つの論理的な値(座標、金額、色、日付の範囲)であること、小さいこと(Microsoftの指針では約16バイト、int 4つほど)、不変であること、継承が不要であることです。構造体が効果を発揮するのは、配列の中の何百万もの点のように、非常に多く作る場合です。要素ごとのヒープの割り当てとガベージコレクターの負担を避けられるからです。

同一性を持つもの(顧客、注文)、大きなもの、複数の場所から変更されるもの、そして迷ったときはクラスを選びます。大きな変更可能な構造体は、コピーのコストと、コピーを変更してしまう混乱の両方をもたらします。

record struct(C# 10)

C# 10では、値の等価性、==、ToString、with への対応を1行で生成する record struct が追加されました。

public readonly record struct Coordinate(double Lat, double Lng);

var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same);            // True
Console.WriteLine(home);                    // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };

普通の record struct の位置指定のプロパティは変更可能で、readonly record struct にするとinit専用になり、通常はこちらが望ましい形です。クラス版とコンパイラが書くメンバーについては、レコードのページで扱います。

よくある間違い

  • コピーを通して構造体を変更する。 list[0].X = 1(CS1612)、foreach の変数(CS1654)、プロパティのgetterの結果。変更したコピーを書き戻すか、構造体を不変にします。
  • 大きな構造体。 代入や呼び出しのたびに全体がコピーされます。フィールドがいくつか以上になると、通常はクラスのほうが速くなります。
  • ゼロにされた既定値を忘れる。 配列と default(T) はコンストラクターを実行せずに構造体の値を作るので、そこで検証しているフィールドでもゼロや null になりえます。
  • 定義せずに == で比較する。 CS0019です。IEquatable<T> と演算子を実装するか、レコード構造体を使います。
  • インターフェースの後ろにある変更可能な構造体。 ボックス化によって、変更は見ていないコピーに対して起きます。

よくある質問

C#の構造体とは何ですか?

構造体はユーザー定義の値型です:struct Point { public int X; public int Y; }。構造体型の変数はオブジェクトへの参照ではなくデータそのものを保持するので、代入やメソッドへの受け渡しで値全体がコピーされます。int、double、DateTime、Guid はすべて構造体です。

C#の構造体とクラスの違いは何ですか?

クラスは参照型で、変数は1つのオブジェクトを共有し、null も許されます。構造体は値型で、各変数が独自のコピーを持ち、(Point? を使わない限り)null にはなれず、継承することもされることもできず、既定値はすべてのフィールドがゼロの状態です。構造体は小さな不変の値に、クラスは同一性と振る舞いを持つエンティティに向いています。

C#でクラスではなく構造体を使うべきなのはどんなときですか?

型が1つの小さな値(座標、金額、日付の範囲)を表し、理想的には不変で、内容で比較され、ヒープの割り当てを避けることが重要になるほど大量に作られる場合です。Microsoftの指針では、さらにサイズがおおよそ16バイト以下であることを挙げています。型が同一性を持つ場合、フィールドが多い場合、参照を通して変更される場合はクラスを使います。

「Cannot modify the return value because it is not a variable」になるのはなぜですか?

これはエラーCS1612で、典型的には構造体の List<Point> に対する list[0].X = 5 から生じます。インデクサーは構造体のコピーを返すので、コピーを変更しても失われてしまい、コンパイラが拒否します。要素を変数にコピーし、変更して、代入し直します:var p = list[0]; p.X = 5; list[0] = p;。array[0] は要素そのものを指すので、配列にはこの問題はありません。

C#のreadonly structとは何ですか?

readonly struct(C# 7.2)は、構造体のどのメンバーも状態を変更しないことを宣言します。すべてのフィールドは readonly、自動実装プロパティは読み取り専用でなければなりません。コンパイラがこれを強制し、構造体を in で渡したり読み取り専用のフィールドに格納したりするときに、防御的なコピーを避けられるようになります。ほとんどの構造体はreadonlyにすべきです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める