🐷

【C#】record(レコード)とは?

2024/12/10に公開

仕事をしていたら突然(というかふと気づいたら)recordなるものが出てきて「なんだこれは?」となったので調べてみました。

レコードとは?

レコードは、主に「不変なデータ(状態)を管理するもの」です。
顧客情報や機械設定などのデータ(状態)を扱う際に使用されます。

C#ではrecord修飾子をつけることで、レコードを作成することができます。
レコードには以下の2つの特徴があります。

値の等価性は「値が同じか?」で判定される

通常のクラスでは「同じインスタンスを参照しているか?」で判断されるのに対し、レコードは「同じインスタンスを参照しているかどうかは関係なく、参照しているインスタンスの値が同じか?」で判断されます。

/// <summary>
/// 顧客情報を表します
/// </summary>
public record CustomerInformation
{
    public int Id { get; init; }
    public string? Name { get; init; }
    public string? Email { get; init; }
}

internal class Program
{
    static void Main(string[] args)
    {
        var customer1 = new CustomerInformation() { Id = 1, Name = "ひよこ", Email = "ABC" };
        var customer2 = new CustomerInformation() { Id = 1, Name = "ひよこ", Email = "ABC" };

        // 別のインスタンスを参照しているので「False」
        Console.WriteLine(Object.ReferenceEquals(customer1, customer2));

        // 別のインスタンスを参照しているが、値は同じなので「True」
        Console.WriteLine(customer1 ==  customer2);
        Console.WriteLine(customer1.Equals(customer2));
    }
}

レコードのイメージとしては、Equals()とGetHashCode()をオーバーライドして値によって等価性を判断するようにしたクラスです。(実際、コンパイル時にそのような変換が行われます。)

不変なデータを扱う

レコードは基本的に不変なオブジェクトとして扱われます。
一度セットした値は後から変更できないようにすることで、データの一貫性を保ちます。

実装のしかたによって値を変更できるように作ることはできますが、レコードの役割である「不変なデータ(状態)を扱う」という役割を満たすために、後で値が変えられないように作るべきです。

例えば以下のようにプロパティのセッターをinitにして、初期化時のみ値を設定できるようにします。

public record class CustomerInformation
{
    public int Id { get; init; }
    public string? Name { get; init; }
    public string? Email { get; init; }
}

initをsetに変更することで後で値を変更できるようにすることができますが、通常の使い方ではありません。

レコードの種類

レコードには「レコードクラス」と「レコード列挙体」の2種類があります。

レコードクラス

レコードクラスは、クラスをレコードにしたものです。
コンパイルされるとクラスに変換されるので結局クラスと同じなのですが、単純にクラスに変換されるわけではなく、Equals()やGetHashCode()、ToString()がオーバーライドされたクラスに変換されます。

レコードクラスはclassキーワードの前にrecord修飾子をつけることで定義します。

public record class CustomerInformation
{
    // 中略
}

レコードクラスは単に「record」と省略して書くこともできます。

public record CustomerInformation
{
    // 中略
}

レコード構造体

※構造体はあまり把握していません。。。
「レコード構造体」は以下のように「record struct」のように省略せずに書きます。

public record struct CustomerInformation
{
    // 中略
}

レコードの使い方

(構造体はあまり詳しくないので)ここではレコードクラスの使いかたについて紹介していきたいと思います。

レコードクラスの定義とインスタンス化

レコードクラスの定義には、プロパティを使う方法と、位置指定パラメタを使う方法があります。

なお、インスタンス化は普通のクラスと同じくnew演算子を使用します。
しかし、レコードクラスの定義のしかたによってインスタンス化の書き方が少し変わります。

普通にプロパティを使用するする方法

// 普通にプロパティを使用する方法
public record CustomerInformation
{
    public int Id { get; init; }
    public string? Name { get; init; }
    public string? Email { get; init; }
}

// インスタンス化にオブジェクト初期化子の構文が使う
var customer1 = new CustomerInformation() { Id = 1, Name = "ひよこ", Email = "ABC" };

位置指定パラメタを使用する方法

// 位置指定パラメタを使用する方法
// 「get; init;」でプロパティが生成されます。
public record CustomerInformation(int Id, string? Name, string? Email)
{
}

// インスタンス化にコンストラクタ(自動生成)を使う。
var customer2 = new CustomerInformation(1, "ひよこ", "ABC");

なお、これらを混ぜた定義の仕方もあります。

プロパティ&位置指定パラメタを使用する方法

public record CustomerInformation(int Id)
{
    public string? Name { get; init; }
    public string? Email { get; init; }
}

var customer1 = new CustomerInformation(1) { Name = "ひよこ", Email = "ABC" };

このコードを見るとわかる通り、プロパティはオブジェクト初期化子で、位置指定パラメタは引数で指定する感じになります。

レコードの複製(DeepCopy)

レコードではwith演算子を使うことで、簡単に複製(DeepCopy)が作成できます。

通常のクラスでは、IDeepCopyインタフェースを実装して再帰的(?)に複製を作っていく。。。など、結構面倒なのですが、レコードではwith演算子を使用するだけでサクッと複製を作ることができます。

/// <summary>
/// 顧客情報を表します
/// </summary>
public record CustomerInformation(int Id, string? Name, string? Email)
{
}

internal class Program
{
    static void Main(string[] args)
    {
        var customer1 = new CustomerInformation(1, "ひよこ", "ABC");
        var customer2 = customer1 with { };

        // 別のインスタンスを参照しているので「False」
        Console.WriteLine(Object.ReferenceEquals(customer1, customer2));

        // 別のインスタンスを参照しているが、値は同じなので「True」
        Console.WriteLine(customer1 ==  customer2);
        Console.WriteLine(customer1.Equals(customer2));
    }
}

注意:変更可能なメンバを持つ場合

with演算子では、クラス型のプロパティが複製されない(参照が共有される)点に注意が必要です。

たとえば以下のようにレコードが普通のクラス型プロパティ(クラス型のメンバ変数)を持っている場合を見てみましょう。with演算子ではこのプロパティの複製はされません。そのため、2人の顧客が同じCardインスタンスを参照していることになり、片方の顧客のカードを変更するともう片方の顧客のカードまで変わってしまうことになります。

/// <summary>
/// カード情報を表します
/// </summary>
public class Card
{
    public string? Color { get; set; }
}

/// <summary>
/// 顧客情報を表します
/// </summary>
public record CustomerInformation(int Id, string? Name, string? Email, Card Card)
{
}

internal class Program
{
    static void Main(string[] args)
    {
        var card = new Card() { Color = "Blue"};

        var customer1 = new CustomerInformation(1, "ひよこ", "ABC", card);
        var customer2 = customer1 with { };

        Console.WriteLine(customer1.Card.Color);    // Blue

        customer2.Card.Color = "Red";

        Console.WriteLine(customer1.Card.Color);    // Red
    }
}

この問題は、不変なデータとして設計されていないことが原因です。

レコードのすべてのプロパティをinitにして、途中で値が変更できないように設計すべきです。

・・・

ちなみに、with演算子では中カッコのなかでプロパティの値を指定することで、その値を持った複製(複製というべきかは微妙ですが)を作ることができます。

var customer1 = new CustomerInformation(1, "ひよこ", "ABC", card);
var customer2 = customer1 with { Id = 2 };

まとめ

  • レコードは「不変あデータ」を管理するために使用する。
  • 等価性は「値の同一性」に基づいて判断される。
  • with演算子で簡単に複製できるが、変更可能なデータには注意が必要。

Discussion