はじめに
プライマリコンストラクタという概念について調べていると元々レコード型で使われていたっぽいので、せっかくなら併せて呑み込んでいくことにしました。
誤りが多分あるので、優しく指摘してくださると嬉しいです。
プライマリコンストラクタ
概要
クラスを宣言する際に、
クラス名の直後に()をつけることで書くことができるコンストラクタ。
元々はレコード型でしか使えなかったが、
C# ver.12からクラスでも使えるようになった。
従来のコンストラクタを使う書き方と、
プライマリコンストラクタを使った書き方を比較してみる。
class Items
{
public string Name;
public int Price;
public Person(string name, int price)
{
Name = name;
Price = price;
}
}
class Items(string name, int price)
{
public string Name = name;
public int Price = price;
}
このようにして簡潔に書くことができる。
プロパティと合わせて書くことももちろん可能。
class Items(string name, int price) {
public string Name { get; } = name;
public int Price { get; } = price;
}
他のコンストラクタとの共存
他のコンストラクタとの併用も可能だが、そのコンストラクタから
プライマリコンストラクタを必ず呼び出さなければならないというルールがある。
例えば次のような場合はエラーになる。
class Items(string name, int price)
{
public string Name = name;
public int Price = price;
public Items() { } // プライマリコンストラクタを呼んでいないのでエラー
}
プライマリコンストラクタを呼びだすには次のようにthis(‥)を使う必要がある。
class Items(string name, int price)
{
public string Name = name;
public int Price = price;
const string DEFAULT_NAME = "品目無し";
const int DEFAULT_PRICE = -1;
// thisを使って呼び出す
public Items() : this(DEFAULT_NAME, DEFAULT_PRICE) { }
public Items(string name) : this(name, DEFAULT_PRICE) { }
}
引数にまつわる注意点
プライマリコンストラクタの引数はクラスのどこからでも参照できる。
フィールドを初期化したり、プロパティを初期化するなど。
何度でも使うことが可能。
初期化に使う場合には特に挙動に変化はない (継承の際にほんの少し変わる程度) が、初期化以外で使う場合には注意が必要となる。
2重フィールド
プロパティやメソッドの中でプライマリコンストラクタの引数を参照した場合、
フィールド変数が生成される。
class Items(int x)
{
public int Property => x;
public void Method() => ++x;
}
// 次のようにフィールドが生成される
class C
{
private int _x;
public Items(int x) => _x = x;
public int Property => _x;
public void Method() => ++_x;
}
このフィールドの生成に絡んだ警告が、以下のようなコードだと発生する。
class Itmes(int x)
{
public int A => x;
public int B { get; } = x;
}
// Bの方の「= x」の部分に警告(CS9124)が出る
// 内部的には次のコードのようになっている
class Items
{
// AとBそれぞれに対応するためフィールドが2つできる
private int _a;
private int _b;
public Items(int x)
{
_a = x;
_b = x;
}
public int A => _a;
public int B { get => _b; }
}
Aのためにxはフィールドとして保存される。
しかしBはそのフィールドを使うわけではなく、
別のフィールドを生成してxの値をコピーする。
「この設計は意図的なものなのか?」という風に警告を発してくる。
警告を回避するために以下のように書く方法がある。
class Items(int x)
{
private readonly int _x = x; // x を一か所に集約
public int A => _x; // フィールド参照
public int B => _x; // こちらも同じフィールド参照
}
書き換えができてしまう
プライマリコンストラクタの引数は「引数」であるがゆえに、
クラス内外から書き換えができてしまう。
class Items(int price)
{
public void Change()
{
price = -9999; // 引数への代入が可能
}
}
// 別ファイル
partial class Items
{
public void Zero => price = 0; // 別ファイルでも書き換え可能
}
しかし先ほどのように、一度readonlyフィールドで受ければ
書き換えを防ぐことができる。
class Items(int price)
{
readonly int _price = price; // 書き換えられない
}
レコード型とは
先に触れた通り、今まで見てきたプライマリコンストラクタは元々レコード型で使えるものだった。以下のようにrecordで定義されるのがそのレコード型である。
この場合はFirstNameという名、LastNameという姓の情報を持った
Personというレコードが生成される。
public record Person(string FirstName, string LastName);
これは次のようにも書くことができる。
public record Person
{
string FirstName { get; init; }
string LastName { get; init; }
}
// initアクセサはオブジェクト初期化時にしか値をセットできない
// 後から書き換えようとするとコンパイルエラーとなる
レコード型は「どういうデータをどういう形式で記録しているのかという情報」が中心にあるのに対し、通常のオブジェクト指向では「外から見た振る舞い」が中心にある。
クラス型との比較
......と言われてもピンと来なかったので、これまでに書いてきたclassとの違いを見ていく。先にまとめると以下のようになる。
class |
record |
|
|---|---|---|
| 振る舞い | 参照型 | 参照型だが、値型的な振る舞い |
| 重視するもの | オブジェクトの同一性を重視 | データの中身を重視 |
| 状態 | 頻繁に変わる | 不変 |
| 用途 | 顧客、注文データなど | 設定、APIレスポンスなど |
| その他 | - | メソッドを自動生成する |
参照の独立性
クラス、レコードをそれぞれ定義し、その違いを見ていく。
まずはクラスを定義してインスタンスをコピーし、Person2.Ageを書き換えてみる。
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var Person1 = new Person{name = "田中", Age = 30};
var Person2 = Person1;
Person2.Age = 31;
Console.WriteLine(Person1.Age)
// 「31」と出力される
次に同じことをレコード型でも行う。
record Person(string Name, int Age);
var Person1 = new Person("田中", 30);
var Person2 = new Person1 with {Age = 31}; // オブジェクトのシャローコピーを生成
Console.WriteLine(Person1.Age);
// 「30」と出力される
クラスは参照型と呼ばれるもので、代入を行う操作は同じオブジェクトへのポインタを渡すという操作になる。そのため、参照先のオブジェクトの中身を書き換えると、それを参照していたオブジェクトも書き換えられてしまう。
一方でレコード (正確にはwith式) は新しいオブジェクトを生成しているため、元のオブジェクトとは無関係なまま存在できる。
class:
Person1 ──┐
├──→ { Name:"田中", Age:31 } ← 同一オブジェクト
Person2 ──┘
record (with式):
Person1 ──→ { Name:"田中", Age:30 } ← 元のオブジェクト(不変)
Person2 ──→ { Name:"田中", Age:31 } ← 新規生成されたオブジェクト
この違いが出力の違いをもたらしている。
プロパティ・コンストラクタの自動生成
レコード型ではプライマリコンストラクタを使って記述した際、プロパティやコンストラクタを自動生成してくれるというメリットがある。
例えば以下のようにレコードを書いたとする。
record Person(string Name, DateTime Birthday);
クラスで同じことをやろうとすると次のような部分を手動で書くことになる。
class Person
{
// プロパティ
public string Name { get; init; }
public DateTime Birthday { get; intit; }
// コンストラクタ
public Person(string Name, DateTime Birthday)
{
this.Name = Name;
this.Birthday = Birthday;
}
// 他にもEqualsやGetHashCodeなどが自動生成される
}
クラスでもプライマリコンストラクタ自体は使えるが、
フィールドやプロパティの宣言は自分で書く必要がある。
だからと言って無用の長物というわけではない。
使うと必ず他のコンストラクタから呼ぶというルールが存在するので、
そこに引数の初期値を書けば、
初期化せずに呼ぶことを防ぐことができる。
参考