C# の event と delegate はセットで登場することが多いですが、どちらが何の役割を担っているのかが混在しやすいです。この記事ではまず delegate から順に整理し、event がなぜ必要なのかの流れで理解していきます。
delegate とは何か
delegate は、メソッドを変数として扱うための型 です。
通常、変数には int や string などの値を入れますが、delegate を使うとメソッドそのものを変数に入れて渡すことができます。
// delegate の型定義(引数と戻り値のシグネチャを指定する)
delegate void Notify(string message);
// この型に合うメソッドを用意する
void SendEmail(string message) => Console.WriteLine($"[Mail] {message}");
void WriteLog(string message) => Console.WriteLine($"[Log] {message}");
// delegate 変数に代入する
Notify handler = SendEmail;
// 呼び出す
handler("処理が完了しました");
// → [Mail] 処理が完了しました
handler という変数に SendEmail メソッドを入れて、後から呼び出しています。メソッドを直接呼ぶのと結果は同じですが、どのメソッドを呼ぶかを実行時に切り替えられるのがポイントです。
マルチキャスト delegate
delegate は += で複数のメソッドを登録でき、呼び出すと全員にまとめて通知できます。これを マルチキャスト と呼びます。
Notify handler = SendEmail;
handler += WriteLog;
handler("処理が完了しました");
// → [Mail] 処理が完了しました
// → [Log] 処理が完了しました
逆に -= で登録を解除することもできます。
handler -= WriteLog;
このマルチキャストの仕組みが、イベント通知の基盤になっています。
event キーワードの役割
delegate だけでも通知の仕組みは作れますが、外部のコードが自由に操作できてしまうという問題があります。
// delegate のままだと…
handler = null; // 外部から全リスナーを消せてしまう
handler("勝手に呼べる"); // 外部から直接発火できてしまう
これを防ぐのが event キーワードです。event をつけると、
+=/-=(登録・解除)は外部からできる= null(上書き)や直接呼び出しは外部からできない
という制限がかかります。
public class Button
{
// event として宣言する
public event Notify? Clicked;
public void Click()
{
// 発火は Button クラス内部からのみ
Clicked?.Invoke("ボタンがクリックされました");
}
}
外部からは登録だけできます。
var button = new Button();
button.Clicked += msg => Console.WriteLine($"[A] {msg}");
button.Clicked += msg => Console.WriteLine($"[B] {msg}");
button.Click();
// → [A] ボタンがクリックされました
// → [B] ボタンがクリックされました
event は「通知専用の delegate」という理解が正確です。
EventHandler と EventArgs の慣習
.NET では event の型として EventHandler を使うのが慣習です。
// EventArgs を継承してデータを持たせる
public class OrderEventArgs : EventArgs
{
public int OrderId { get; init; }
public string ProductName { get; init; } = "";
}
public class OrderService
{
// EventHandler<T> を使う
public event EventHandler<OrderEventArgs>? OrderPlaced;
public void PlaceOrder(int id, string name)
{
// ... 注文処理 ...
// 購読者に通知する
OrderPlaced?.Invoke(this, new OrderEventArgs
{
OrderId = id,
ProductName = name
});
}
}
受け取り側は次のように登録します。
var service = new OrderService();
service.OrderPlaced += (sender, e) =>
Console.WriteLine($"[通知] 注文 {e.OrderId}: {e.ProductName}");
service.PlaceOrder(1001, "ノートPC");
// → [通知] 注文 1001: ノートPC
sender は通知を発行したオブジェクト、e がイベントデータです。この形は .NET のコアライブラリでも広く使われているため、慣習に合わせて書いておくと他の開発者にも読みやすくなります。
Func / Action との違い
delegate の宣言が面倒な場合は、.NET 組み込みの汎用 delegate である Action / Func が使えます。
| 型 | 意味 |
|---|---|
Action<T> |
引数あり・戻り値なしの delegate |
Func<T, TResult> |
引数あり・戻り値ありの delegate |
EventHandler<T> |
(object sender, T e) 形式の通知用 delegate |
自前で delegate 型を定義するのは、特別なシグネチャが必要な場合に限られます。多くの場合は Action / Func / EventHandler で対応できます。
まとめ
delegateはメソッドを変数として扱う型。マルチキャストで複数登録できるeventはdelegateに「外部から勝手に上書き・発火できない」制限を加えたもの- 通知を受け取る側は
+=で登録、-=で解除 - .NET の慣習として
EventHandler<TEventArgs>の形を使うのが一般的
event を理解すると、.NET の UI フレームワークや各種ライブラリのイベント購読がスムーズに読めるようになります。