class / struct / record の使い分け

文法 難易度: ★★★☆☆(中級)

前提知識

先にこちらを読んでおくと、この記事がすらすら読めます。

class / struct / record はどれも独自の型を定義するキーワードだが、代入したときの挙動・==の既定・書き換え可否がそれぞれ違う。structが値型としてコピーされる仕組みは「構造体(struct)の基本」、recordの値等価とwith式は「レコード(record)の基本」で説明した通りなので、ここでは3つを並べて比較し、使い分けの判断基準だけを扱う。

観点 class struct record
代入したときの挙動 実体を共有する(参照のコピー) 値ごとコピーされる 既定はclassと同じ参照型。値型にしたいならrecord struct
==の既定 参照等価(同じ実体かどうか) 定義されていない。値の比較にはEqualsを使う 値等価(全プロパティが一致すればtrue)
書き換え可否 setのあるプロパティは生成後も書き換え可能 setのあるプロパティは書き換え可能。影響はコピー先だけ 既定はinit専用で生成後は書き換え不可。withで新しいインスタンスを作る

動かしてみる

var c1 = new PointClass { X = 1, Y = 1 };
var c2 = c1;
c2.X = 100;
Console.WriteLine(c1.X);
Console.WriteLine(c1 == c2);

var s1 = new PointStruct { X = 1, Y = 1 };
var s2 = s1;
s2.X = 100;
Console.WriteLine(s1.X);
Console.WriteLine(s1.Equals(s2));

var r1 = new PointRecord(1, 1);
var r2 = r1 with { X = 100 };
Console.WriteLine(r1.X);
Console.WriteLine(r1 == r2);

public class PointClass
{
    public int X { get; set; }
    public int Y { get; set; }
}

public struct PointStruct
{
    public int X { get; set; }
    public int Y { get; set; }
}

public record PointRecord(int X, int Y);
100
True
1
False
1
False

c2c1と同じ実体を指すので、c2.Xを変えるとc1.Xも100になり、==も参照等価でtrueになる。s2s1の値をコピーして作られるため書き換えてもs1は1のままだが、struct==演算子を既定で持たないので値の比較にはEqualsを使う。recordのプロパティは既定でinit専用のため直接書き換えられず、withで新しいインスタンスを作る。値が異なるr1r2を比較すると、record==は値等価なのでfalseになる。

最低限の理解

⚠️ 可変なstructをList<T>の要素として直接書き換える

List<T>の要素がstructのとき、points[0]で取り出せるのは値のコピーだ。そのプロパティにそのまま代入しようとすると、コピーへの代入とみなされコンパイルエラーになる。

var points = new List<PointStruct> { new PointStruct { X = 1, Y = 1 } };
points[0].X = 100; // NG: List<T>の要素は値のコピーなので直接書き換えられない

public struct PointStruct
{
    public int X { get; set; }
    public int Y { get; set; }
}
error CS1612: 変数ではないため、'List<PointStruct>.this[int]' の戻り値を変更できません
var points = new List<PointStruct> { new PointStruct { X = 1, Y = 1 } };
var updated = points[0];
updated.X = 100;
points[0] = updated; // OK: 新しい値を作って丸ごと置き換える
Console.WriteLine(points[0].X);

public struct PointStruct
{
    public int X { get; set; }
    public int Y { get; set; }
}
100

課題

自分が扱うデータ(注文明細、座標、ユーザー情報など)を1つ選び、classstructrecordのどれで定義するのが適切か理由とともに判断し、実際に定義してみる。

次に読む