1. はじめに
プログラムを扱うと、ある部分が他の部分に「何か重要なことが起きた」と通知しなければならない状況に必ず直面します。典型的な例は、ユーザーがマウスをクリックして、そのイベントを処理する必要がある場合です。日常生活でもイベントの世界に生きています:キッチンでやかんが鳴ったらシグナルを聞いてコンロを消す。コーヒーがキーボードにこぼれたら心臓がドキッとしてノートパソコンを救おうとする。プログラミングも同じ法則に従います。
イベントとは、ソースオブジェクト(パブリッシャー)が起きた変更やアクションについて他のオブジェクト(サブスクライバー)に知らせる仕組みです。いわば「知らせたら、聞いていた人が反応する」という感じです。
C#ではイベントはデリゲート型に基づく特別な構文です。デリゲートはコールバックのシグネチャを定義します—サブスクライバー側で何がどのように呼ばれるかを決めます。イベントの宣言はキーワード event、デリゲートはキーワード delegate で行います。
なぜイベントが必要なのか?
- 疎結合:パブリッシャーはサブスクライバーについて何も知らない—ただ信号を送るだけ。
- アーキテクチャの柔軟性:パブリッシャーのコードを変えずにハンドラを動的に追加・削除できる。
- スケーラビリティ:新しいサブスクライバーを追加すれば、すぐに通知を受け取れる。
古典的な「パブリッシャー-サブスクライバー」パターン
「火災報知器」クラス(パブリッシャー)と「建物の人」クラス(サブスクライバー)があると想像してみてください。アラームが鳴ると同時に全員に信号を送ります—建物に何人いてどこにいるかは関係ありません。これが「パブリッシャー-サブスクライバー」(またはObserver)パターンです。
パブリッシャーは何人のどんなサブスクライバーがいるかを知らない—ただ通知するだけで、残りは自分で購読するか無視するか決めます。
C#ではどう動くのか?
- パブリッシャー:イベント(event)を定義し、購読/解除を提供する。
- サブスクライバー:イベントに購読し、ハンドラを実装する(イベント発生時に呼ばれるメソッド)。
2. 実践:最初の例
家からコードに移って、最も単純なモデルを説明します。コンソールアプリケーションがあって、タイマーオブジェクトが毎秒「ティック」して、いくつかのハンドラが反応する(例えばコンソールに「ティック」を表示したり、ティック数を数えたり)とします。
ステップ1. デリゲートとイベントの定義
public class SimpleTimer
{
// イベント用のデリゲートを宣言する
public delegate void TickEventHandler(object sender, EventArgs e);
// デリゲートに基づくイベント
public event TickEventHandler Tick;
public void Start(int count)
{
for (int i = 0; i < count; i++)
{
System.Threading.Thread.Sleep(1000); // ティックの模擬!
OnTick(); // イベントを発火!
}
}
protected virtual void OnTick()
{
// 購読者がいるならイベントを呼び出す (Tick != null)
Tick?.Invoke(this, EventArgs.Empty);
}
}
ここで何が起きているか?
- 古典的なシグネチャである object sender, EventArgs e を持つデリゲート TickEventHandler を定義している。
- イベント Tick はハンドラの購読ポイント。
- Start メソッドは「ティック」を模擬し、タイマーごとに OnTick を呼び出す。
- OnTick では安全にイベントを呼ぶ:Tick?.Invoke(..., EventArgs.Empty)。
ステップ2. イベントの購読
class Program
{
static void Main()
{
var timer = new SimpleTimer();
// Tick イベントに購読する
timer.Tick += Timer_Tick;
timer.Start(3);
// 必要なら解除もできる
timer.Tick -= Timer_Tick;
}
static void Timer_Tick(object sender, EventArgs e)
{
Console.WriteLine("ティック!");
}
}
タイマーを作って、演算子 += で購読し、ティックごとにハンドラが呼ばれる。解除は演算子 -= を使う。
3. 役に立つニュアンス
なぜイベントは「ハードコール」より良いのか?
もし SimpleTimer が OnTick の中で直接コンソールに書き込んでいたら、そのクラスは特定のアクションに強く結合されてしまう。イベントを使えばコードは解放される:タイマーはサブスクライバーが何をするか知らない—ロケットを発射するか、ファイルにログを書くか、メールを送るかは関係ない。
イベントとデリゲートの重要な違い
- デリゲートはメソッドへの「ポインタ」、一方で イベント はアクセス制限のあるデリゲート。
- サブスクライバーは購読/解除しかできない;イベントを外部から呼ぶことはできない—それはパブリッシャーだけができる。
- イベントを宣言するにはデリゲート型に event 修飾子を付ける—コンパイラが適切なアクセスモデルを提供する。
C#におけるイベントの簡単な動作図
+------------------+ +------------------------------+
| | | |
| パブリッシャー | <------> | サブスクライバー |
| (Publisher/Event)| | (Subscriber/Handler) |
| | | |
+------------------+ +------------------------------+
| 1) イベントを宣言 | 2) それを購読する
| 3) イベントを発火 | 4) ハンドラを実装する
いつイベントを使うべきか?
- 不特定多数のリスナーに何かが起きたことを知らせたいとき。
- ソースクラス内でアクションのロジックを結び付けたくないとき。
- UI、非同期インタラクション、システム通知—あらゆるところでイベントが使われる。
C#でのイベント実装に関する短いポイント
- イベントは外部から呼べない:呼べるのはパブリッシャーのコードだけで、Invoke を使う。
- 購読/解除:演算子 +=/-=;ハンドラは複数登録できる。
- イベントは実際はデリゲートのリスト:イベントが起きると、登録順に全てのハンドラが呼ばれる。
- 推奨されるデリゲート:互換性のために EventHandler と EventHandler<TEventArgs> を使うことを推奨。
4. 初心者がやりがちな典型的なミス
購読者がいるかどうかをチェックするのを忘れる:Tick != null。安全な呼び出し(Tick?.Invoke(...))を使う方が良い。
イベントに購読したまま、ハンドラが不要になったときに解除しない。これによりオブジェクトがメモリに残り、メモリリークにつながることがある。
外部クラスからイベントを「呼ぼう」とする—コンパイラは許可しない。イベントがメソッドでない場合、たとえば game.GameOver() のようには書けない。
デリゲートのシグネチャを守らない。イベントには標準の EventHandler や EventHandler<TEventArgs> を使うと、他の .NET ライブラリと互換性が保てる。
GO TO FULL VERSION