先月くらいからじわじわと、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 の場合はプロパティまでは作らない、キャプチャが掛からない限りフィールドにすらしないという案あり
- record の場合はプライマリ コンストラクター引数から
みたいな実装のようです。
この辺りは 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>
の実装も需要が高いことは認識してて検討の範囲内