目次

キーワード

概要

C# 9.0 で、レコード型(records)という新しい種類の型が追加されました。 record (記録)という名前通り、データの読み書きに使うことを意図した型です。 例えば以下のような書き方で、「Name という文字列と Birthday という日付」を読み書きできます。

using System;
 
record Person(string Name, DateTime Birthday);

データが主役のプログラミング

プログラミングをしていると、データが主役・データが中心になる場面がちらほらあります。 「データが主役」(data centric)というのは、例えば以下のように、「Name という文字列と、Birthday という日付を持っている」というような「何の型がどういうデータを記録しているか」という情報が強い意味を持つような場面です。

using System;
 
class Person
{
    public string Name;
    public DateTime Birthday;
}

オブジェクト指向ではこれと真逆の考え方をしたりします。 「実装の隠ぺい」などで書いていますが、 オブジェクト指向では「内部的にどういうデータを持っているか」はあまり重要ではなく、 「外から見てどうふるまうか」の方が主役(behavior centric)になります。 例えば上記の例に当てはめるなら、Birthday (誕生日)はなんだったら int (単なる整数。例えば独自の基準日からの経過日数で表すなど)でデータを記録していてもよくて、「外から見て DateTime 型で Birthday を取れれば中身は何でもいい」みたいに考えます。

ところが、データを保存・復元したり、ネットワーク越しに送受信したり、GUI で表示・編集する場合、結局「どういうデータをどういう形式で持っているか」という内部的な情報がそのまま必要になったりします。 例えば上記の Person 型であれば、以下のような JSON 形式で保存して、これを読み書きしたりすることが結構あると思います。

{
  "name": "天馬飛雄",
  "birthday": "2003/04/07"
}

ボイラープレートなコード

上記の例に挙げた Person 型の作りは、話を単純化するために簡素化して書いたものですが、 これを「C# のお作法的に好ましい書き方」で書こうとすると実は結構なコード量を書く必要があります。 例えば以下のようなコードになります。

class Person : IEquatable<Person>
{
    public string Name { get; init; }
    public DateTime Birthday { get; init; }
 
    public Person(string name, DateTime birthday)
    {
        Name = name;
        Birthday = birthday;
    }
 
    public bool Equals(Person? other)
        => other is { } person &&
            Name == person.Name &&
            Birthday == person.Birthday;
 
    public override bool Equals(object? obj)
        => obj is Person person &&
            Name == person.Name &&
            Birthday == person.Birthday;
 
    public override int GetHashCode()
        => HashCode.Combine(Name, Birthday);
 
    public override string ToString()
        => $"Person {{ Name = {Name}, Birthday = {Birthday} }}";
}

このコードで書いているのは以下のようなものです。

  • immutable (1度作ったデータは書き換え不可能にする)にした方がいい
    • C# (8.0 以前)で immutable なデータ型を作ろうと思うとコンストラクターが必要になる
    • そうなると、コンストラクターの引数と、プロパティへの代入で同じ名前を何度も書く必要がある
  • 参照型であっても値的にふるまう(value semantics を持つ)ようにしたい
    • 2つのインスタンスが「全て同じプロパティ値を持つなら等しい」という判定にする
    • 等値判定(Equals メソッドや GetHashCode メソッド)を書く必要がある
  • 必須ではないものの、ToString で中身のデータが見れると便利

これは、どんな「データ中心の型」でもほぼ同じものを書く必要があり、かなり定型コードです。 あまりにも同じものを繰り返し書かないといけないので「ボイラープレート」(boilerplate: 焼き型製品。タイ焼きとかみたいな同じ形状のものを大量生産するやつ)と呼ばれたりします。

当然、毎度毎度同じようなコードを繰り返し書かないといけないのはしんどいです。 「お作法的に好ましいものを書こうとするとボイラープレートがしんどい」という状態はあまり好ましくなく、 簡潔に書ける専用の文法が求められていました。

レコード型

このボイラープレート問題を解決するために、データ中心の型向けの新しい構文として導入されたのがレコード型です。 最も短い書き方をすると、例えば以下のようになります。

record Person(string Name, DateTime Birthday);

クラスとの差は以下の2点です。

  • class キーワードの代わりに record キーワードを使う
  • 型名の後ろに直接コンストラクター引数を書ける(プライマリ コンストラクター)
    • この場合、コンストラクター引数と同名のプロパティが自動的に作られる

ちなみに、「プライマリ コンストラクター」(後述)は使わずに、以下のように書くこともできます。

record Person
{
    public string Name { get; init; }
    public DateTime Birthday { get; init; }
}

どちらの書き方でも、record キーワードを使って定義した型に対しては、 以下のようなものが自動生成されます。

  • EqualsGetHashCode== などの等値判定メソッド
    • IEquatable<T> インターフェイスの実装も含む
  • ToString メソッド
  • 後述する with 式で使うクローン メソッド
  • 後述する派生クラス判別のための EqualityContract プロパティ

要するに、前述した「お作用的に好ましい」とされるコードを一通りコンパイラーが用意してくれます。

レコード型とクラス

ちなみに、コンパイラーが色々と自動生成してくれる以外は通常のクラスと同じというか、 内部的には実際にただのクラスとしてコンパイルされます。 コンパイラー生成のコードをそのまま書くと以下のような感じになります。 (一部、実際には手書きの C# コードでは書けないメソッド名で生成されていたりしますし、 C# コンパイラーのバージョンによって微妙に異なるコードになったりはしますが、 意味的にはほぼこのままのコードになります。)

class Person : IEquatable<Person>
{
    protected virtual Type EqualityContract => typeof(Person);
 
    public string Name { get; init; }
    public DateTime Birthday { get; init; }

    public Person(string Name, DateTime Birthday)
    {
        this.Name = Name;
        this.Birthday = Birthday;
    }
    
    public void Deconstruct(out string Name, out DateTime Birthday)
    {
        Name = this.Name;
        Birthday = this.Birthday;
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Person");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }
 
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Name");
        builder.Append(" = ");
        builder.Append((object)Name);
        builder.Append(", ");
        builder.Append("Birthday");
        builder.Append(" = ");
        builder.Append(Birthday.ToString());
        return true;
    }
 
    public static bool operator !=(Person r1, Person r2) => !(r1 == r2);
    public static bool operator ==(Person r1, Person r2) => (object)r1 == r2 || (r1 is not null && r1.Equals(r2));
 
    public override int GetHashCode()
        => (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295
        + EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295
        + EqualityComparer<DateTime>.Default.GetHashCode(Birthday);
 
    public override bool Equals(object obj) => Equals(obj as Person);
 
    public virtual bool Equals(Person other)
        => (object)other != null
        && EqualityContract == other.EqualityContract
        && EqualityComparer<string>.Default.Equals(Name, other.Name)
        && EqualityComparer<DateTime>.Default.Equals(Birthday, other.Birthday);
 
    public virtual Person Clone() => new Person(this);
 
    protected Person(Person original)
    {
        Name = original.Name;
        Birthday = original.Birthday;
    }
}

等値判定

前節で示した通り、レコード型を書くと自動的に ==Equals などの等値判定用のメソッド・演算子が追加されます。

ここで1点注意なんですが、レコード型から作られる ==EqualsEqualityComparer.Default を経由して、最終的にはプロパティごとに Equals メソッドを呼ぶことになります。 通常、==Equals の結果が違うなんてことは求められないので、 そんな実装になっていることはめったにはありませんが… 悪名高いものとして、NaN (Not a Number)は ==Equals の結果が違ったりするので、 「double で直接比較」と「レコード型で1段包んで比較」の結果が変わったりします。

using System;
 
double nan = double.NaN;
 
Console.WriteLine(nan == nan); // 常に false を返す。
Console.WriteLine(nan.Equals(nan)); // こっちは true だったりする。
 
var recordNan = new Double(nan);
 
Console.WriteLine(recordNan == recordNan); // true
Console.WriteLine(recordNan.Equals(recordNan)); // true
 
record Double(double Value);

record class と record struct

(C# 10.0 予定)

C# 9.0 (レコード型の最初のバージョン)では、レコード型は常に参照型になります。 これに対して C# 10.0 では値型も選べるようになる予定です。 そのため、以下のような書き分けができるようになる予定です。

// C# 10.0 予定
record class Reference(int X, int Y); // record だけ書いた場合こちらと同じ意味
record struct Value(int X, int Y);

レコード型と匿名型

導入順序的な問題で、C# 的に言うとレコード型は「名前のある匿名型」みたいな感じ(トゲナシトゲトゲっぽいやつ)だったりはします。 (匿名型が C# 3.0 で導入された機能なのに対して、レコード型は C# 9.0 です。)

C# は既存機能と新機能の整合性を極力取るように頑張って機能追加をしていて、 レコード型追加の際には匿名型やタプルとの整合性を結構気にして設計しています。

例えば以下のようなコードがあったとします。 これは「とりあえず型名は付けずに匿名(タプル)でコードを書いてみた」みたいな状態です。

using System;
 
var p = (X: 1, Y: 2); // これはタプル
var (x, y) = p;
Console.WriteLine($"{x} * {y} = {x * y}");

ここで、X, Y のペアに Point という名前を付けたいとして、そのためにレコード型を使ったとします。

record Point(int X, int Y);

先ほどのコードには、new Point を足すだけで「匿名の型から名前付きの型に移行」ができます。

using System;
 
var p = new Point(X: 1, Y: 2); // これで名前付きになった
var (x, y) = p;
Console.WriteLine($"{x} * {y} = {x * y}");

プライマリ コンストラクター

先ほど、レコード型の最も短い書き方として以下のような例を挙げました。

record Person(string Name, DateTime Birthday);

この、型名の直後に () でコンストラクター引数を並べる書き方をプライマリ コンストラクターと言います。

名前にコンストラクターと入っている通り、この構文からコンストラクターがコンパイラー生成されます。 それだけではなく、引数名と同名の init-only プロパティが生成されます。 前述の「レコード型から生成されるクラスの例」で挙げたコードのうち、 以下のものは「プライマリ コンストラクターからの生成物」になります。

class Person
{
    public string Name { get; init; }
    public DateTime Birthday { get; init; }

    public Person(string Name, DateTime Birthday)
    {
        this.Name = Name;
        this.Birthday = Birthday;
    }
    
    public void Deconstruct(out string Name, out DateTime Birthday)
    {
        Name = this.Name;
        Birthday = this.Birthday;
    }
}

残りの EqualsPrintMember などについては、プライマリ コンストラクターの有無にかかわらず、 レコード型であれば常にコンパイラー生成されます。 (public な get アクセサーを持ったすべてのプロパティが「生成元」になります。)

ちなみに、プライマリ コンストラクターの引数は、以下のように、 他のメンバーの初期化子や、メソッドの中身で参照することもできます。

record X(int Value)
{
    public int Squared { get; } = Value * Value;
    public int Cubed { get; } = Value * Value * Value;
    public double GetSqrt() => Math.Sqrt(Value);
}

また「プライマリ」(primary: 一番の、最優先の)の名前の通り、 このコンストラクターは特別というか、「手書きで他のコンストラクターを書き足す場合、必ずプライマリ コンストラクターが呼ばれるようにしなければならない」という強い制約が掛かります。 例えば以下のコードはコンパイル エラーを起こします。

record Person(string Name, DateTime Birthday)
{
    // プライマリ コンストラクターを呼んでいないのでコンパイル エラーになる。
    public Person() { }
}

以下のように this 初期化子を足せばコンパイルできるようになります。

record Person(string Name, DateTime Birthday)
{
    // これならエラーにはならない。
    // ("" や default をとりあえず入れてしまう行為の良し悪しは置いておいて…)
    public Person() : this("", default(DateTime)) { }
}

プライマリ コンストラクター引数への属性付与

プライマリ コンストラクターからはプロパティがコンパイラー生成されます。 また、プロパティからもさらにフィールドが生成されています。 ということで、プライマリ コンストラクターの引数には3重の意味(引数、プロパティ、フィールド)が含まれていたりします。

必要となる場面はそれほど多くはないと思いますが、 コンパイラー生成されるプロパティやフィールドに対しても、 以下のような書き方で属性を付けることができます。

using System;
using System.Reflection;
 
var t = typeof(X);
 
// parameter
Console.WriteLine(t.GetConstructor(new[] { typeof(int) }).GetParameters()[0].GetCustomAttribute<A>().Name);
 
// property
Console.WriteLine(t.GetProperty("Value").GetCustomAttribute<A>().Name);
 
// field
Console.WriteLine(t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)[0].GetCustomAttribute<A>().Name);
 
record X(
    [A("parameter")]
    [property: A("property")]
    [field: A("field")]
    int Value);
 
class A : Attribute
{
    public string Name { get; }
    public A(string name) => Name = name;
}

生成物の上書き

レコード型の目標の1つは「お作法として最も好ましいものを最も短い書き方で書ける」というものです。

一方で、わかった上でお作法から外れたい場合や、ちょっとしたカスタマイズをしたい場合もあります。 そこで、C# のレコード型では、コンパイラー生成されるであろう物と同名のプロパティやメソッドを手書きすると、 手書きした方のコードが優先される仕様になっています。

例えば、文字列の比較で大文字・小文字を無視したい場合、Equals メソッドなどを以下のように書き加えることでできます。 (例と言うことで Equals のみを書きますが、実際は GetHashCode などの書き足しも必要です。)

record Person(string Name, DateTime Birthday)
{
    bool IEquatable<Person>.Equals(Person? other)
        => other is not null
        && Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
        && Birthday == other.Birthday;
}

他の例として、本来あまり好ましくはないんですが、プロパティを書き換え可能にしてしまいたい場合、 以下のようにプロパティを手書きで足してしまうことでできます。

record Person(string Name, DateTime Birthday)
{
    public string Name { get; set; } = Name;
    public DateTime Birthday { get; set; } = Birthday;
}

ちなみに、この場合、「Name 引数を Name プロパティに自動代入する」みたいな処理は掛からないので注意が必要です。 上記の例で Name { get; } = Name; としているように、明示的な初期化が必要になります。

レコード型の継承

レコード型はクラスと同じく継承ができます。 例えば以下のように書けます。

record Base;
record A(int N) : Base;
record B(string S) : Base;

ちなみに、基底クラスがプライマリ コンストラクターを持っている場合、 派生クラスからそのプライマリ コンストラクターの呼び出しが必要です。 以下のように、基底クラス名の後ろに () を付けることで呼び出せます。

record Base(int X);
 
// Base(int X) を呼んでいないのでエラーになる。
record Error1(int X) : Base;
 
// コンパイルできる例1: 引数を伝搬。
record Ok1(int X) : Base(X);
 
// コンパイルできる例2: 引数に何らかの既定値を与える。
record Ok2() : Base(1);
 
// コンパイルできる例3: 引数を伝搬しつつ、プロパティ Y を追加。
// (この場合、X は Base.X が優先されて、Ok3.X はコンパイラー生成されない。)
record Ok3(int X, int Y) : Base(X);
 
// Ok2 と似たようなものだけど、これはコンパイルできない。
// コンパイラーの実装都合で () が必要。
record Error2 : Base(1);

また、将来的なことを言うと、以下のような話もあります。

  • C# 9.0 時点ではレコード型の派生はレコード型同士でしかできない
    • 将来的にはクラスからレコード型、レコード型からクラスの派生も認めるかもしれない
  • record struct が入るとすると、派生できるのは record class のみ
    • (単に record と書くと record class の意味なので派生できる)

ちなみに、「レコード型とクラス」で書いたコンパイラー生成物に EqualityContract というプロパティがありますが、 これは派生クラスを弁別するためにあります。

例えば以下のコードの出力結果は false です。

using System;
 
// 同じ値を持っていても型が違うと「不一致」扱い。
Console.WriteLine(new Base(1) == new Derived(1)); // false
 
record Base(int X);
record Derived(int X) : Base(X);

コンパイラー生成物の Equals 等が EqualityContract == other.EqualityContract みたいなコードを含んでいるのはこのためです。 ちなみに、ちゃんと new Base(x).Equals(new Derived(y))new Derived(y).Equals(new Base(x)) の結果が一致するように作られています。 (これが成り立つようにするのは意外と手間で、レコード型からのコンパイラー生成物は結構なコード量になっていますし、手書きメンバー追加でのカスタマイズは結構大変です。)

逆に、型は無視してプロパティの一致だけで等価判定してほしい場合、以下のように EqualityContract を手書き追加すればできます。

using System;
 
// EqualityContract で typeof(Base) を返すことで「一致」扱いに変わる。
Console.WriteLine(new Base(1) == new Derived(1)); // true
 
record Base(int X);
record Derived(int X) : Base(X)
{
    protected override Type EqualityContract => typeof(Base);
}

with 式

「お作法として好ましいのは immutable (書き換え不可)」と言いましたし、 レコード型もそのお作法に則って(手書きでカスタマイズしない限り) immutable な型を生成します。

immutable なデータに対しても、「データの一部分だけを書き換えたい」という要件はよくあるんですが、 この場合「元のデータを書き換えず、クローンして一部分書き換えたデータを新規作成」という手順を踏みます。 C# 8.0 以前の書き方でいうと、以下のようなコードを書くことになります。 (が、1つ問題があって、実際には C# 8.0 で完全にこれと同じことは実現できません。)

using System;
 
var p1 = new Person("天馬飛雄", new(2003, 4, 7));
 
var p2 = p1.Clone();
p2.Name = "鉄腕アトム";
 
record Person(string Name, DateTime Birthday);

このコードなんですが、(immutable というお作法上)「Name は書き換え不能」で作られているはずです。 その書き換え不能プロパティを書き換えないといけないので、C# 8.0 でこれと同様のコードは書けません。 これが init-only プロパティという機能の導入の動機で、C# コンパイラーが「クローン直後」だけを特別扱いして書き換えを認めています。

C# 9.0 ではどうしたかと言うと、with 式という構文を追加しました。 これが内部的に上記の「まずクローンして、クローン直後のインスタンスの一部分を書き換える」という処理に展開されます。

var p1 = new Person("天馬飛雄", new(2003, 4, 7));
var p2 = p1 with { Name = "鉄腕アトム" };

とりあえず以下のように覚えてください。

  • immutable なデータに対して一部分だけの書き換えをしたいときには with 式を使う
  • 内部的にやっていることはクローンしてからの部分書き換え

ちなみに、with { } だけ書く(「一部書き換え」はしない)のでもクローンになります。

using System;
 
var p1 = new Point(1, 2);
var p2 = p1 with { }; // p1 のクローンが作られる
 
Console.WriteLine(p1 == p2); // 持ってる値的には等しいので true。
Console.WriteLine(ReferenceEquals(p1, p2)); // クローン(別インスタンス)ができてるのでこちらは false。
 
record Point(int X, int Y);

また、C# 9.0 時点ではクローン用のメソッドを with 式以外から呼ぶ手段はありません。 (<Clone>$ みたいな通常の C# コードでは書けないような変な名前でクローン メソッドが生成されています。) 将来的には認める可能性もあるものの、現状は「クローン方法のカスタマイズ」もできません (Equals 等と違って、<Clone>$ メソッドを「手書きで上書き」はできません)。

また、このクローン用メソッドを手書きできない都合上、C# 9.0 時点では with 式はレコード型専用の構文になります。 (ただし、C# 10.0 で struct に対しては with 式を使えるようになる予定です。 構造体は元からクローン相当の機能を持っているので、それを使う予定です。)

匿名型と構造体に対する with 式

(C# 10.0 予定)

C# 9.0 時点では、with 式はレコード型に対してしか使えません。 ただ、with 式に求められる要件は「クローンメソッドを持っていて、set もしくは init 持ちのプロパティがある」という点だけです。 前述の通り、「クローン方法のカスタマイズ」を提供することで任意の型に対して with 式を使えるようにするという提案も出ていますが、その一方で、現状でも with 式が使える条件を満たせるけどもスケジュールの都合で C# 10.0 での実装になったものもあります。

(現状の予定では C# 10.0 で) 匿名型と、任意の構造体に対して with 式を使えるようになる案が出ています。 以下のコードは C# 9.0 ではコンパイルできませんが、将来、コンパイルできるようになるはずです。

匿名型:

var p1 = new { X = 1, Y = 2 };
var p2 = p1 with { X = 3 };

任意の構造体:

var p1 = new Point { X = 1, Y = 2 };
var p2 = p1 with { X = 3 };
 
readonly struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

更新履歴

ブログ