レコード(record)の基本

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

前提知識

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

recordは、単純なデータ型を簡潔に定義するためのキーワードだ。主な目的として、次の性質を満たすようなデータ型を定義するために使われる。

なぜ必要か

DTO(データの受け渡しだけを目的にした型)や値オブジェクトのように、中身が同じなら同じものとして扱いたい型は多い。クラスで同じ動作をさせるにはEqualsGetHashCodeToStringを自分で書く必要があり、型が増えるたびに同じコードを繰り返すことになる。recordはこの繰り返しを1行にまとめる。

動かしてみる

var user = new User("Taro", 20);
var other = new User("Taro", 20);
Console.WriteLine(user);
Console.WriteLine(user == other);

public record User(string Name, int Age);
User { Name = Taro, Age = 20 }
True

record User(string Name, int Age);という1行で、NameAgeを持つ型ができる。newで別々に作ったuserotherは、プロパティの値が同じなので==はTrueになる。

同じUserをクラスで書くと、省略されていた中身が見える。

var user = new UserClass("Taro", 20);
var other = new UserClass("Taro", 20);
Console.WriteLine(user);
Console.WriteLine(user.Equals(other));

public class UserClass
{
    public string Name { get; }
    public int Age { get; }

    public UserClass(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public override bool Equals(object? obj) =>
        obj is UserClass other && Name == other.Name && Age == other.Age;

    public override int GetHashCode() => HashCode.Combine(Name, Age);

    public override string ToString() => $"UserClass {{ Name = {Name}, Age = {Age} }}";
}
UserClass { Name = Taro, Age = 20 }
True

recordが自動生成しているのは、このEqualsGetHashCodeToStringと、値を受け取って読み取り専用のプロパティに設定するコンストラクタだ。手書きのクラスはEqualsを上書きしても==演算子は既定で参照比較のままなので、上のコードではEqualsメソッドを呼んで比較している。

最低限の理解

var user = new User("Taro", 20);
var older = user with { Age = 21 };
Console.WriteLine(older);
Console.WriteLine(user == older);

public record User(string Name, int Age);
User { Name = Taro, Age = 21 }
False

現場での注意

C# 9より前(.NET Framework、C# 7/8など)のプロジェクトではrecordは使えない。そうした現場に当たったときは、上のUserClassのような手書きクラスがそのまま本番の書き方になる。なおrecordstructとして定義するrecord structという書き方もあるが、使い分けは次の記事に譲る。

⚠️ 配列やListをプロパティに持つと値等価が効かない

recordEqualsは、配列プロパティに対しては配列自体のEqualsを呼ぶだけになる。中身の要素が同じでも、別の配列インスタンスなら「等しくない」と判定される。

// NG: 配列プロパティは中身が同じでも参照が違うと等しくならない
var a = new Order("1", new[] { "apple" });
var b = new Order("1", new[] { "apple" });
Console.WriteLine(a == b);

public record Order(string Id, string[] Items);
False

値で比較したいなら、EqualsGetHashCodeを自分で書き足す。実装方法は「オブジェクトの等価性を正しく実装する」の通りで、リストの中身を1件ずつ比較する。

// OK: Equalsを上書きしてリストの中身まで比較する
var a = new Order("1", new List<string> { "apple" });
var b = new Order("1", new List<string> { "apple" });
Console.WriteLine(a == b);

public record Order(string Id, IReadOnlyList<string> Items)
{
    public virtual bool Equals(Order? other) =>
        other is not null && Id == other.Id && Items.SequenceEqual(other.Items);

    public override int GetHashCode() => Id.GetHashCode();
}
True

課題

自分の業務でよく扱うデータ(注文、商品など)を1つ選んでrecordで定義し、newで2つ作って==で比較してみる。次に、同じ動作をする手書きクラスに書き換えて、何行増えるか数えてみる。

次に読む