C# 14 の partial コンストラクターとイベント
C# 14 ではインスタンスコンストラクターとイベントを partial メンバーとして宣言でき、定義をファイル間で分割することで、よりすっきりしたコード生成と関心の分離を実現できます。
C# 14 では、インスタンスコンストラクター と イベント を partial メンバーとして宣言できる新機能が導入されました。これにより、partial クラスの 2 つの部分にまたがってコンストラクターやイベントの定義を分割できます。これは、C# が長年サポートしてきた partial メソッドや partial プロパティと同様の仕組みです。partial クラスの一方の部分でコンストラクターやイベントを 宣言 し、もう一方の部分で 実装 することができます。これは、片方のファイルが自動生成され、もう片方が開発者によって手動で保守されるようなコード生成のシナリオで特に有用です。
C# 14 の partial コンストラクター
partial コンストラクター を使うと、partial クラス内でクラスのコンストラクターを 2 つの宣言に分割できます。一方の宣言はシグネチャだけを提供し (定義宣言)、もう一方は実際の本体を提供します (実装宣言)。コンパイラーがこれらを統合し、実行時には単一のコンストラクターとして振る舞うようにします。
partial コンストラクターの主なルール:
- 定義宣言と実装宣言はそれぞれちょうど 1 つずつ: partial クラスの一方の部分にコンストラクターを宣言する箇所 (本体を持たずセミコロンで終わる) と、もう一方に実装を提供する箇所 (コンストラクターのコードブロックを持つ) を用意する必要があります。シグネチャ (パラメーターの型、名前など) は両者で完全に一致しなければなりません。定義側または実装側のいずれかが欠けている場合 (あるいは重複している場合)、コードはコンパイルできません。
- partial クラス内でのみ: 両方の宣言は同じクラスにあり、そのクラス自体に
partialが付いている必要があります。partial でないクラスに partial コンストラクターを置くことはできません。 - コンストラクター初期化子は実装側にのみ: コンストラクターが
: this(...)や: base(...)を使って別のコンストラクターや基底クラスのコンストラクターを呼び出す必要がある場合、その初期化子は 実装宣言にのみ 書けます。定義側 (シグネチャのみの部分) には、いかなるコンストラクター初期化子も書けません。実際には、this(...)やbase(...)の呼び出しはすべて実装側コンストラクターのヘッダーに書きます。 - プライマリコンストラクターの構文は 1 ファイルにのみ: C# 14 では partial クラスでも プライマリコンストラクター (クラス宣言上のコンストラクターパラメーター) を使えますが、パラメーターリストを含められるのは partial ファイルのうちの 1 つだけです。つまり、partial クラスで簡潔なプライマリコンストラクター形式を使う場合、それは単一の partial クラス宣言に置く必要があり、同じクラスの他の partial 宣言は独自のパラメーターリストを繰り返したり宣言したりしてはいけません。
以下は partial コンストラクターの実例です。クラスが 2 つのファイルに分割され、自動生成された部分がコンストラクターを宣言し、もう一方の部分がそれを実装している様子を想像してください。
// File: Car.AutoGenerated.cs (auto-generated partial class)
public partial class Car
{
// Defining partial constructor (signature only, no body)
public partial Car(string model);
}
// File: Car.cs (developer-maintained partial class)
public partial class Car
{
// Implementing partial constructor (actual body)
public partial Car(string model)
{
// (Optional) Can call another constructor or base constructor here
// : base() or : this(...) would be placed after the parameter list if needed.
if (string.IsNullOrEmpty(model))
throw new ArgumentException("Model must be provided", nameof(model));
Console.WriteLine($"Car model set to {model}");
}
}
この例では、Car クラスのコンストラクターが分割されています。最初の部分は Car(string model) が存在することを宣言し、2 番目の部分が引数を検証してメッセージを出力する実装を提供します。定義宣言にはコードを含められず、; で終わるシグネチャだけです。実装宣言には本体があり、(Car が他のクラスを継承している場合は) 基底クラスへの呼び出しや、this() で別のコンストラクターを呼ぶこともできます。コンパイル時にコンパイラーは各部分がちょうど 1 つずつ存在することを確認し、コンストラクターを単一の統一されたものとして扱います。
C# 14 の partial イベント
partial イベント を使うと、partial クラス内でイベントの定義を同様に分割できます。クラスの一方の部分でイベントを定義し (通常の自動イベント宣言と同じく add/remove のロジックは持たない)、もう一方の部分が add/remove アクセサー (購読者がイベントハンドラーを追加または削除したときに実行されるコード) を提供します。
partial イベントの主なルール:
- 定義宣言と実装宣言はそれぞれちょうど 1 つずつ: コンストラクターと同様に、
partialが付いたイベントは partial クラス内でちょうど 2 回宣言される必要があります。本体のない定義 1 つと、アクセサー本体を持つ実装 1 つです。両者は同じイベント型と名前を持ち、同じ partial クラスに現れる必要があります。これら 2 つを超える partial イベントの片割れがあってはいけません。 - 定義宣言はフィールド形式: partial イベントの定義部分は、フィールドに裏打ちされた通常のイベント宣言と同じように書きます。たとえば
public partial event EventHandler SomethingHappened;のように、セミコロンで終わりアクセサーブロックを持ちません。ただし、partial であるため、コンパイラーはこのイベントに対してバッキングフィールドや既定の add/remove ロジックを自動生成しません。要するに「この名前と型のイベントが存在する」と示すプレースホルダーです。 - 実装宣言にはアクセサーが必要: partial イベントの実装部分には、手動で実装したイベントと同じく、イベントブロック内に
addとremoveの両方のアクセサーを記述する必要があります。リスナーが購読・解除したときの動作はここに書きます。定義部分には暗黙のバッキングフィールドがないため、実装は通常、購読者の格納を自分で管理する (たとえばプライベートフィールド、リスト、その他の仕組みで) か、別のイベントソースに転送する必要があります。 - partial クラス内のみ (abstract やインターフェイス実装としては不可): partial コンストラクターと同様に、partial イベントは
partialが付いたクラス内でしか使えません。partial イベントをabstractにすることはできず、インターフェイスのイベントをpartialで実装することもできません。この機能はクラス内での分割を意図したもので、インターフェイスの境界をまたぐものではありません。
partial イベントを示す例を以下に挙げます。一方のファイルがイベントを宣言し、もう一方がカスタムの add/remove ロジックを実装します。
// File: Sensor.AutoGenerated.cs (auto-generated partial class)
public partial class Sensor
{
// Defining partial event (no body, just declaration)
public partial event EventHandler DataReceived;
}
// File: Sensor.cs (developer-maintained partial class)
public partial class Sensor
{
// Implementing partial event (with custom add/remove logic)
private EventHandler _dataReceivedHandlers; // backing storage for event handlers
public partial event EventHandler DataReceived
{
add
{
Console.WriteLine("Subscriber added to DataReceived");
_dataReceivedHandlers += value;
}
remove
{
Console.WriteLine("Subscriber removed from DataReceived");
_dataReceivedHandlers -= value;
}
}
// Optional: a method to raise the event safely from this class
protected void OnDataReceived(EventArgs e)
{
_dataReceivedHandlers?.Invoke(this, e);
}
}
この例では、Sensor クラスの DataReceived イベントが 2 つのファイルに分かれています。クラスの自動生成された部分は、EventHandler 型の DataReceived イベントが存在することを宣言します。開発者の側の部分は、購読者を保持するプライベートフィールドを定義し、メッセージを記録してそのフィールドを更新する add と remove ブロックを実装することで、実際の実装を提供します。他のコードが sensor.DataReceived += Handler; を実行すると、カスタムの add ロジックが走ります。同様に、購読の解除ではカスタムの remove ロジックが走ります。クラスは OnDataReceived メソッドなどから _dataReceivedHandlers を使ってイベントを発火できます。実装部分がなければ、定義宣言だけではコンパイルできません。イベント自体には裏付けがないため、C# はイベントの機能を完成させる実装部分を必要とします。
以前の C# バージョンとの比較
C# 14 より前は、コンストラクターやイベントを partial として宣言することはできませんでした。C# 13 およびそれ以前では、partial キーワードはクラス、メソッド、(C# 13 以降は) プロパティ/インデクサーにしか使えませんでした。コンストラクターやイベントを partial にしようとすると、コンパイラーはエラーを出していました。この制限のため、コンストラクターやイベントの実装をファイル間で分割する手段がありませんでした。
開発者は同様の柔軟性を得るために、しばしば回避策を用いていました。たとえば、自動生成コードがオブジェクト構築中に何らかのカスタムロジックを呼び出す必要があるシナリオを考えてみてください。partial コンストラクターがなかった時代の一般的な手法は、コンストラクター内で partial メソッド を使うことです。クラスの自動生成された部分がそのメソッドを呼び出し、メソッドの実装は別のファイルに置く、というやり方です。古い C# コードでは、たとえば次のような形が見られました。
// Before C# 14: using a partial method to extend constructor logic
public partial class Car
{
public Car()
{
InitializeComponents(); // auto-generated initialization
OnConstructed(); // call a partial method hook
}
// This partial method can be implemented in another file to add custom behavior
partial void OnConstructed();
}
2 番目のファイルで、開発者は InitializeComponents() のあとに追加コードを実行するために partial メソッド OnConstructed を実装できました。このパターンは構築時にカスタムコードを差し込めるものの、間接的でややぎこちないものでした。同様にイベントの場合は、簡単な回避策がなく、イベントの add/remove ロジックをファイル間で分割できないため、機能を継承して上書きするか生成コードを書き換えるかしか選択肢がありませんでした。
C# 14 の partial コンストラクターとイベントによって、こうした回避策はもう必要ありません。コンストラクターやイベントを直接 partial として宣言し、実装を別途提供できるので、コードはより素直になります。新しい方法はわかりやすく、ダミーの partial メソッドやその他のトリックを必要としません。
ユースケースとメリット
partial コンストラクターとイベントの主な恩恵を受けるのは、コード生成とツール連携 に関わるシナリオです。多くのフレームワークやツールは C# コードを生成し (たとえば UI デザイナー、ORM、インターフェイスのスキャフォールディングツールなど)、開発者が自動生成コードを編集せずに生成クラスを拡張できるよう、クラスを partial として印を付けることがよくあります。C# 14 では:
- コードジェネレーターがコンストラクターを定義してくれる: たとえばソースジェネレーターが partial コンストラクターのシグネチャを作成し、開発者がカスタム初期化ロジックで実装する、ということができます。逆に、ジェネレーターが、開発者が宣言した partial コンストラクターを実装し、フレームワーク固有のセットアップコードを裏側で差し込むこともできます。これにより、生成された初期化とユーザーが書いた初期化を安全に組み合わせやすくなります。
- 複雑な構築ロジックの分割: 大規模プロジェクトでは、コンストラクターのロジックを複数のファイルにまたがって整理したい場合があります (たとえば依存関係の組み立てとビジネスロジックを分けるなど)。必要に応じて、partial コンストラクターでこれを構造的に行えます。
- カスタムイベントパターン: partial イベントは高度なイベント処理シナリオを可能にします。代表例が、メモリリーク防止のための 弱イベントパターン です。開発者やソースジェネレーターは、片方のファイルで partial イベントを (たとえば
[WeakEvent]のような属性付きで) 宣言し、別のファイルでadd/removeがハンドラーへの弱参照を使うように実装できます。これにより、購読側オブジェクトが解除を忘れてもガベージコレクションを妨げません。partial イベント以前は、弱イベントパターンの実装には大量の決まり文句や外部ライブラリが必要でした。今ではライブラリ作者が、複雑な add/remove ロジックを別の partial 実装で自動的に提供するジェネレーターを用意し、利用者のコードはきれいなまま保つことができます。同様に、partial イベントを使って、低レベル API のイベントをラップしたうえで add/remove を処理し、実際のソースに接続するように、購読を下位のシステムへ転送することもできます。 - プラットフォームと相互運用コード: Xamarin やネイティブライブラリとの .NET 相互運用などのシナリオでは、生成されたクラスがコンストラクターでネイティブコードを呼び出したり、ネイティブイベントの接続を管理したりする必要があることがよくあります。partial コンストラクターを使えば、ネイティブ呼び出しのセットアップは生成コードに置き、コンストラクターの残りの部分はユーザー定義にする (またはその逆) ことができます。partial イベントを使えば、クラスが .NET イベントを公開しつつ、その実体は別の partial ファイルで実装されたプラットフォーム固有のイベント機構に支えられている、という構成も可能です。
これらすべてのケースで、partial コンストラクターとイベントはコードのメンテナンスを容易にします。自動生成コードは「何が存在すべきか」を宣言でき、手書きのコードは「開発者が本当に実装したいこと」だけに絞れます。partial メンバーの使用に実行時のパフォーマンスコストはなく、分割はコンパイル時にのみ存在します。生成されるプログラムは、コンストラクターやイベント全体を 1 か所に書いた場合とまったく同じように振る舞います。
インスタンスコンストラクターとイベントを partial にできるようにしたことで、C# 14 は partial クラス機能をほぼすべてのメンバー型に対応する形へと拡張しました。この強化により、自動生成コードとカスタムコードのきれいな分離を、言語としてより支援できるようになります。開発者はクラスの振る舞いを小手先の手段なしで整理・拡張する柔軟性を得られ、ソースジェネレーターは機能をモジュール的に注入する強力な新しい手段を手に入れます。大規模なフレームワークに取り組む場合でも、ただ可読性のためにコードを分けたい場合でも、partial コンストラクターとイベントは C# 14 でより表現力が高く便利なアプローチを提供します。
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.