先月くらいからじわじわと、C# Language Design Meeting で Records がらみの議題が上がっています。 最近やっとまとまってきた感じがするのでまとめて紹介。

record 型の新設

まず、基本方針として、record は class/struct に対する修飾子ではなくて、enum とか delegate とかと同じく1種の型みたいな扱いにしたみたいです。 なので、以下のような書き方に。

record Point(int X, int Y);

とりあえず初期実装としては結構やることを絞るみたいで、

  • record は参照型
    • 値型なものは既存の struct に手を入れるか、"record struct" を新設するかになると思うもののまだ未定
  • プライマリ コンストラクターを持てるのは record だけ
    • class Point(int X, int Y) とか struct Point(int X, int Y) とかは未実装
    • 検討はされてるものの、record と同じコード生成をすべきかどうかでまだ迷ってそう
      • record の場合はプライマリ コンストラクター引数から public int X { get; init; } プロパティを作ることが決まってる
      • 通常の class, struct の場合はプロパティまでは作らない、キャプチャが掛からない限りフィールドにすらしないという案あり

みたいな実装のようです。

この辺りは issue のコメントでの反発も結構大きいんですが… 修飾子じゃなくて型のカテゴリーの新設な点とか、当初実装に値型版がない点とか…

構造体との一貫性

今はいったん未定な状態になってるんですが、 仮に、普通の class/struct にもプライマリ コンストラクターを持てて、 record のものと近いコード生成をすることになったとします (1案としてはそういう実装も考えられます)。

じゃあ、class と record の本質的な差は何になるかと言うと、

  • メンバーごとの(shallow な)比較による Equasl/GetHashCode が生成される
  • メンバーごとの(shallow な)コピーによる clone メソッドが生成される

という点になります。 で、この2つ、struct の場合は標準で作られます。

using System;
 
struct Point
{
    public int X;
    public int Y;
}
 
class Program
{
    static void Main()
    {
        var p1 = new Point { X = 1, Y = 2 };
        var p2 = new Point { X = 1, Y = 2 };
        Console.WriteLine(p1.Equals(p2)); // true
 
        p2.X = 3;
        Console.WriteLine(p1.Equals(p2)); // false
    }
}

ということで、コンセプト上は、「record は struct のような振る舞いを持つ参照型」みたいに考えることもできます。 なので、今の struct の挙動とあまりに違うものにはしたくないし、 今の struct が非効率な実装になっちゃってる部分は record に合わせて struct の方にも改善を入れてもいいかもとか、 そういう感じの話は出ています。

data 修飾子

プライマリ コンストラクター前提の構文は「positional record」と呼ばれています。 引数の並びに意味があって、new Point(1, 2) みたいに、positional(位置指定) で初期化ができるためこう呼びます。

一方で、プロパティを元にして、new Point { X = 1, Y = 2 } みたいに書く想定のものを「nominal record」と呼びます。 nominal record のために、data 修飾子も用意する流れのようです。 以下のような書き方ができます。一見、data 修飾子を付けたフィールドっぽい書き方ですが、get; init; な public プロパティが生成されます。

record Point
{
    data int X;
    data int Y;
}

base 呼び出しとか、プライマリ コンストラクター引数のスコープとか

あとは細かい話。 record 型は派生もできるんですが、その場合、以下のような書き方ができます。

record Person(string FirstName, string LastName)
{
    public string Fullname => $"{FirstName} {LastName}";
    public override string ToString() => $"{FirstName} {LastName}";
}
 
record Student(string FirstName, string LastName, int Id)
    : Person(FirstName, LastName)
{
    public override string ToString() => $"{FirstName} {LastName} ({ID})";
}

このとき、以下のような点が検討に上がっています。

  • コンストラクター引数に対して、それと同名のプロパティと、引数からプロパティへの代入コードが自動生成される
    • 代入のタイミングは base コンストラクターより前であるべきか後であるべきか
    • 今のところ「前」案優勢
  • 基底クラスのコンストラクターを呼んでいる部分(この例だと Person(FirstName, LastName) の引数の部分のスコープはどうなるべきか
    • クラス内の全メンバーがスコープ
    • ただ、通常コンストラクターのbase アクセスと同様に、インスタンス メンバーに触わろうとするとエラー
  • 自動生成されるのと同名のメンバーを手書きすると、手書きの方を優先して使う
    • Equals とか
    • その手書き Equals とかが sealed だったりするとエラーにする
  • object.Equals(object) じゃなくて Equals(T) は作るべきか? → そうする予定だし、IEquatable<T> の実装も需要が高いことは認識してて検討の範囲内