CodeGym /コース /C# SELF /recordとclass、structの違い

recordとclass、structの違い

C# SELF
レベル 19 , レッスン 4
使用可能

1. そもそもなんでrecordが必要だったの?

C#(と.NET全体)では、長い間class(class)とstruct(struct)がメインのビルディングブロックだった。でも、それぞれちょっと足りないところがあるんだ。classは参照型でミュータブル、比較は参照でやる(ごくまれに例外あり)。structは値型(渡すときにコピーされる)、デフォルトでバイト単位の比較、たいていミュータブル(readonly structが出るまでは特に)。

でも、もし「シンプルで自然なデータの記録がしたい」「値で比較したい」「サクッとクローンしたい」「誰かにどこかでオブジェクトを変えられる心配したくない」って思ったら、自分で色々工夫するか、ValueTupleとかSystem.Tupleみたいなライブラリを使うしかなかった。でも、どれもイマイチしっくりこないし、表現力も足りないんだよね。

だからC# 9でrecordが登場したんだ。これは宣言が短くて、不変性が安全で、値比較に特化したデータ型だよ。

2. 4つの型を1ページで比較!

class struct record record struct
カテゴリ 参照型 値型 参照型 値型
ミュータブルか デフォルト:はい デフォルト:はい デフォルト:いいえ(init) デフォルト:いいえ(init)
比較 参照で(== 値で(フィールド) 値で(fields/properties) 値で(fields)
クローン 手動のみ 手動のみ 組み込みサポートあり(with 組み込みサポートあり(with
継承 あり なし あり なし
不変性 自分で実装必要 自分で実装必要 めっちゃ簡単に実装できる めっちゃ簡単に実装できる
構文 一番長い 短い 一番短い(ポジショナル) かなり短い
コレクションでの使い方 参照 コピー 参照 コピー

ビジュアル図解

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. recordclassstructの違いの本質

メモリ上の挙動:参照か値か?

  • classrecordは参照型。関数に渡すときはオブジェクトへの参照がコピーされる。
  • structrecord structは値型。常にバイト単位でコピーされる(明示的に参照渡ししない限り)。

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // pc.Yが変わる!
    ChangeY(ps);    // ps.Yは変わらない ― コピー!
    ChangeY(pr);    // pr.Yが変わる!
    ChangeY(prs);   // prs.Yは変わらない ― コピー!
}

void ChangeY(dynamic p) { p.Y = 99; }

ここでdynamicが魔法みたいに見えるかもだけど、これは単なる例だよ。structだと値が変わらないけど、classやrecord(参照型)だと変わるってことを見せたかっただけ。

比較:2つのオブジェクトが等しいかどうか?

  • class:参照で比較(== ― メモリ上で同じオブジェクトならtrue)、Equalsをオーバーライドしない限り。
  • struct:全フィールドの値で比較(デフォルト)。
  • record:主コンストラクタで指定した全フィールド/プロパティの値で比較。
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // False! 別オブジェクト
Console.WriteLine(bar1 == bar2); // True! 値が同じ

面白い豆知識:
record structはさらにすごいよ:普通のstructみたいに「値で比較」だけど、recordの構文や機能も使える。

4. 不変性:どこまで保証できる?

オブジェクトの安全性を比べてみよう:

  • class:デフォルトだと簡単に変更できる。全部のフィールドをreadonlyにしないとダメ。
  • struct:同じだけど、readonly structにすればフィールドもプロパティも変更不可。
  • record:たいていフィールドはinit修飾子付き。つまりコンストラクタかオブジェクト初期化子(with)でしかセットできない。データの安全な受け渡しに便利。
  • record struct:同じく、readonly record structにすれば値型の不変性+recordの便利機能が手に入る。

record Person(string Name, int Age);

var p1 = new Person("アレクセイ", 23);
// p1.Age = 24; // エラー!initのみ

var p2 = p1 with { Age = 24 }; // OK!新しいデータでコピー作成

大きなプロジェクトでエンティティオブジェクトがたくさんあるとき、recordを使えば「誰かがどこかでフィールドを変えてバグった」みたいな事故を防げるよ。

5. 構文:宣言の見た目と混乱しないコツ


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
各型の宣言構文の比較

見ての通り、record型が一番短く書けるよ。コンストラクタ、デストラクタ、等価比較、ToString()とか、色々「最初から」ついてくる。

構文 無料でついてくるもの 継承できる?
class ほぼ何もなし あり
struct ほぼ何もなし なし
record ToString, Equals, Deconstruct, with あり
record struct ToString, Equals, Deconstruct, with なし

6. クローンとwith演算子

recordrecord structだけが、with演算子を持ってる。これで、プロパティの一部だけ変えて簡単にコピーできるよ。


record User(string Name, int Age);

var user1 = new User("イリーナ", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "イリーナ", user2.Age == 29

class(class)の場合はコピー用メソッドを自分で書かないといけないし、フィールドのコピーを忘れるとバグの元だよ。

7. 継承:どれがどれを継承できる?

  • class:普通の継承ができる(クラス階層、仮想メソッド、抽象化など)。
  • struct:継承できない(インターフェースの実装だけ)。
  • record:継承できるけど、いくつか制限あり(record型同士のみ、classとは不可)。
  • record struct:継承できない、普通のstructと同じ。
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // OK!

class Vehicle { }
class Car : Vehicle { } // OK!

// structはstructから継承できない

継承については次のレベルで詳しくやるよ :P

8. どの型がどんな場面で使われる?

  • class:大きなオブジェクトで、リッチな振る舞い、長いライフサイクル、ミュータブルな状態、階層構造(例:ビジネスロジック、UIコンポーネント)。
  • struct:小さな値オブジェクトで、超高速コピー、GCなし、オーバーヘッド最小(例:座標、色、金額―サクッとコピーできるもの)。
  • record:DTO(Data Transfer Object)、パラメータオブジェクト、設定パラメータ、不変状態、計算結果など、中身で比較したいもの。
  • record struct:値型で不変性が欲しい、recordの挙動が欲しい、でもヒープに余計なアロケーションしたくないとき。

9. よくあるミスと落とし穴

たまにプログラマーがrecordを「classの代わり」と思っちゃうけど、それは違う!オブジェクトの状態を途中で変えたいならclassを使おう。
参照で比較したい(例:シングルトンパターンや、オブジェクトのライフサイクルが超重要なとき)はclassを使おう。
数値や座標みたいな値オブジェクトならstructrecord struct
不変で比較しやすいオブジェクトをアプリの層間で渡したり、コレクションに入れたり、ログやシリアライズしたいならrecordがベスト。

あと、recordクラスで読み取り専用プロパティだけにしても、ネストしたオブジェクトを忘れると、そっちはミュータブルなら普通に変更できちゃうから注意!


record Student(string Name, List<int> Grades);

var s1 = new Student("アントン", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // 両方のオブジェクトが「2」で呪われる!s1.Grades == s2.Grades
1
アンケート/クイズ
DTOの問題、レベル 19、レッスン 4
使用不可
DTOの問題
DTO、record、そしてwith
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION