C# eventとは?初心者がつまずく仕組み・使い方・EventHandlerとの違いをサンプルで解説
はじめに
C#のeventは、初心者がつまずきやすい機能のひとつです。
「delegateと何が違うのか」
「EventHandlerとは何なのか」
「なぜ外部から直接呼び出せないのか」
「+=や-=は何をしているのか」
このような疑問を持つ人は少なくありません。
C# eventは、ボタンクリック、処理完了、状態変更、エラー通知など、「何かが起きたこと」を別の処理へ知らせるための仕組みです。特にWindows Forms、WPF、ASP.NET、Unity、独自クラス設計など、さまざまな場面で登場します。
この記事では、C#のeventについて、初心者でも理解しやすいように、delegateとの関係、基本構文、EventHandlerとの違い、実装サンプル、よくあるエラーまで順番に解説します。
1. C#のeventとは?まず押さえるべき基本
1-1. eventは「何かが起きたことを通知する」ための仕組み
C#のeventは、あるクラス内で発生した出来事を、別のクラスや処理に通知するための仕組みです。
たとえば、次のような場面で使われます。
ボタンがクリックされた
ファイルの読み込みが完了した
データの更新が終わった
エラーが発生した
ユーザーの入力内容が変わった
C# eventを使うと、「イベントを発生させる側」と「イベントを受け取って処理する側」を分けて書けます。
たとえば、処理完了を通知するクラスがあるとします。
C#public class Worker
{
public event Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を実行中...");
Completed?.Invoke();
}
}
このCompletedがイベントです。
DoWorkメソッドの処理が終わったタイミングで、Completed?.Invoke()によってイベントを発生させています。
1-2. ボタンクリックや処理完了で使われるイベント駆動の考え方
C# eventを理解するうえで重要なのが、イベント駆動という考え方です。
イベント駆動とは、「何かが起きたら、それに応じて処理を実行する」というプログラムの作り方です。
Windows Formsなどでよく見るボタンクリック処理は、典型的なイベント駆動です。
C#button1.Click += Button1_Click;
private void Button1_Click(object? sender, EventArgs e)
{
Console.WriteLine("ボタンがクリックされました");
}
このコードでは、ボタンがクリックされたときにButton1_Clickメソッドが呼び出されます。
ポイントは、こちらから直接Button1_Clickを呼び出しているのではなく、「クリックされた」というイベントに反応して処理が実行されることです。
このように、C# eventは「起きた出来事に応じて処理を実行する」ために使われます。
1-3. eventを使うと呼び出し元と処理側を分離できる
eventを使う大きなメリットは、クラス同士の依存を減らせることです。
たとえば、処理を行うWorkerクラスが、処理完了後にメール送信やログ出力をしたい場合を考えます。
eventを使わない場合、Workerクラスの中にメール送信処理やログ出力処理を直接書くことになります。
C#public class Worker
{
public void DoWork()
{
Console.WriteLine("処理を実行中...");
Console.WriteLine("ログを出力しました");
Console.WriteLine("メールを送信しました");
}
}
この書き方だと、Workerクラスが「処理」だけでなく「ログ」や「メール」のことまで知ってしまいます。
eventを使うと、Workerは「処理が完了した」と通知するだけで済みます。
C#public class Worker
{
public event Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を実行中...");
Completed?.Invoke();
}
}
ログを出すか、メールを送るか、画面に表示するかは、イベントを受け取る側が自由に決められます。
C#var worker = new Worker();
worker.Completed += () => Console.WriteLine("ログを出力しました");
worker.Completed += () => Console.WriteLine("メールを送信しました");
worker.DoWork();
このように、eventを使うと「通知する側」と「通知を受けて処理する側」を分離できます。
1-4. 初心者がeventでつまずきやすいポイント
C# eventで初心者がつまずきやすいポイントは、主に次のとおりです。
| つまずきやすい点 | 理由 |
|---|---|
| delegateとの違いが分かりにくい | eventはdelegateをベースにしているため |
+=と-=の意味が分かりにくい | メソッドをイベントに登録・解除しているため |
| 外部からイベントを呼び出せない | eventは外部からの直接実行を制限するため |
EventHandlerとの違いが分かりにくい | eventは仕組み、EventHandlerは型だから |
| nullでエラーになる | 登録されたハンドラがない状態で呼び出すと問題になるため |
| 購読解除を忘れる | 長寿命オブジェクトとの組み合わせでメモリリークにつながるため |
eventは一度にすべてを理解しようとすると難しく感じます。
まずは、「eventは何かが起きたことを通知する仕組み」と押さえておきましょう。
2. C# eventの仕組みをdelegateとの関係から理解する
2-1. eventはdelegateをベースにした仕組み
C#のeventは、delegateをベースにした仕組みです。
delegateとは、メソッドを変数のように扱うための型です。
たとえば、次のようにdelegateを定義できます。
C#public delegate void NotifyHandler();
このdelegateは、「戻り値がvoidで、引数を持たないメソッド」を参照できます。
C#public class Sample
{
public NotifyHandler? Notify;
public void Run()
{
Notify?.Invoke();
}
}
このように、delegateにはメソッドを登録して呼び出すことができます。
C#var sample = new Sample();
sample.Notify = () => Console.WriteLine("通知されました");
sample.Run();
eventは、このdelegateに対して「外部からできる操作を制限したもの」と考えると理解しやすいです。
2-2. delegateだけでイベント処理を書くと何が問題になるのか
delegateだけでも、イベントのような処理は書けます。
C#public class Worker
{
public Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を実行中...");
Completed?.Invoke();
}
}
使う側は次のようにメソッドを登録できます。
C#var worker = new Worker();
worker.Completed += () => Console.WriteLine("処理が完了しました");
worker.DoWork();
一見問題なさそうですが、delegateをpublicフィールドとして公開すると、外部から自由に操作できてしまいます。
たとえば、外部から代入できます。
C#worker.Completed = null;
これにより、すでに登録されていた処理がすべて消えてしまいます。
さらに、外部から直接呼び出すこともできます。
C#worker.Completed?.Invoke();
本来、処理完了イベントはWorkerクラスの処理が終わったときに発生すべきです。
しかしdelegateをそのまま公開すると、外部の好きなタイミングでイベントのような処理を実行できてしまいます。
これはクラス設計として安全ではありません。
2-3. eventを付けることで外部からできること・できないこと
eventを付けると、外部からできる操作が制限されます。
C#public class Worker
{
public event Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を実行中...");
Completed?.Invoke();
}
}
この場合、外部からできるのは基本的に次の2つです。
C#worker.Completed += Handler;
worker.Completed -= Handler;
つまり、イベントハンドラの登録と解除です。
一方で、外部から次のような操作はできません。
C#worker.Completed = null; // エラー
worker.Completed?.Invoke(); // エラー
eventを付けることで、イベントを発生させる権限は、そのeventを宣言しているクラス内に限定されます。
これが、delegateだけでなくeventを使う大きな理由です。
2-4. eventとdelegateの違いを比較表で整理
eventとdelegateの違いを整理すると、次のようになります。
| 項目 | delegate | event |
|---|---|---|
| 役割 | メソッドを参照する型 | 出来事を通知する仕組み |
| ベース | それ自体が型 | delegateを利用する |
| 外部からの登録 | できる | できる |
| 外部からの解除 | できる | できる |
| 外部からの代入 | できる | できない |
| 外部からの呼び出し | できる | できない |
| 主な用途 | コールバック、関数の受け渡し | イベント通知 |
| 安全性 | 公開方法に注意が必要 | イベント用途に適している |
初心者向けに一言でいうと、delegateは「呼び出せるメソッドの型」、eventは「delegateを安全にイベント通知として使うための仕組み」です。
3. C# eventの基本的な書き方と使い方
3-1. eventの宣言方法
C# eventの基本的な宣言は、次の形です。
C#public event デリゲート型? イベント名;
たとえば、Actionを使う場合は次のようになります。
C#public event Action? Completed;
独自delegateを使う場合は、先にdelegate型を定義します。
C#public delegate void CompletedHandler();
public class Worker
{
public event CompletedHandler? Completed;
}
また、.NETでよく使われる標準的な形として、EventHandlerを使う方法もあります。
C#public event EventHandler? Completed;
初心者のうちは、まずActionを使ったシンプルなeventで流れを理解し、その後にEventHandlerを学ぶと理解しやすいです。
3-2. イベントハンドラを登録する「+=」の使い方
イベントに処理を登録するには、+=を使います。
C#worker.Completed += OnCompleted;
このOnCompletedのように、イベント発生時に呼び出されるメソッドをイベントハンドラと呼びます。
C#static void OnCompleted()
{
Console.WriteLine("処理が完了しました");
}
ラムダ式で登録することもできます。
C#worker.Completed += () =>
{
Console.WriteLine("ラムダ式で処理完了を受け取りました");
};
+=は、イベントに対して「この処理も呼び出してほしい」と登録するイメージです。
複数のイベントハンドラを登録することもできます。
C#worker.Completed += () => Console.WriteLine("ログを出力");
worker.Completed += () => Console.WriteLine("メールを送信");
worker.Completed += () => Console.WriteLine("画面に表示");
イベントが発生すると、登録された処理が順番に呼び出されます。
3-3. イベントハンドラを解除する「-=」の使い方
イベントハンドラの登録を解除するには、-=を使います。
C#worker.Completed -= OnCompleted;
サンプルで見ると、次のようになります。
C#var worker = new Worker();
worker.Completed += OnCompleted;
worker.DoWork();
worker.Completed -= OnCompleted;
-=は、イベントに登録した処理を外すための構文です。
注意点として、ラムダ式を直接登録した場合、同じ形に見えるラムダ式を書いても解除できないケースがあります。
C#worker.Completed += () => Console.WriteLine("完了");
worker.Completed -= () => Console.WriteLine("完了"); // 基本的には解除できない
解除する可能性がある場合は、メソッドとして定義するか、ラムダ式を変数に入れておきます。
C#Action handler = () => Console.WriteLine("完了");
worker.Completed += handler;
worker.Completed -= handler;
3-4. イベントを発生させる基本コード
イベントを発生させるには、イベントを宣言しているクラスの中で呼び出します。
C#public class Worker
{
public event Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を開始します");
Console.WriteLine("処理中...");
Completed?.Invoke();
}
}
Completed?.Invoke()の部分でイベントを発生させています。
イベントに登録されたハンドラがあれば呼び出され、登録がなければ何も起きません。
3-5. nullチェックとInvokeを使った安全な呼び出し方
eventは、ハンドラが1つも登録されていない場合、nullになります。
そのため、次のように直接呼び出すと例外が発生する可能性があります。
C#Completed();
安全に呼び出すには、nullチェックを行います。
C#if (Completed != null)
{
Completed();
}
現在のC#では、null条件演算子を使って次のように書くのが一般的です。
C#Completed?.Invoke();
EventHandlerの場合は、次のようになります。
C#Completed?.Invoke(this, EventArgs.Empty);
?.Invoke()を使うことで、イベントハンドラが登録されていない場合でも安全に処理できます。
4. サンプルコードで学ぶC# eventの実装手順
4-1. 処理完了を通知するシンプルなeventサンプル
ここでは、処理完了を通知するシンプルなC# eventのサンプルを作ります。
C#public class Worker
{
public event Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を開始します");
Console.WriteLine("処理中...");
Console.WriteLine("処理が完了しました");
Completed?.Invoke();
}
}
このWorkerクラスは、DoWorkメソッドの最後でCompletedイベントを発生させています。
4-2. イベントを受け取る側のクラスを作成する
次に、イベントを受け取る側のクラスを作成します。
C#public class Logger
{
public void OnCompleted()
{
Console.WriteLine("Logger: 処理完了を記録しました");
}
}
このOnCompletedメソッドを、WorkerのCompletedイベントに登録します。
C#var worker = new Worker();
var logger = new Logger();
worker.Completed += logger.OnCompleted;
worker.DoWork();
実行結果は次のようになります。
処理を開始します
処理中...
処理が完了しました
Logger: 処理完了を記録しました
WorkerはLoggerの存在を直接知りません。
ただイベントを発生させているだけです。
この分離がeventの重要なメリットです。
4-3. event発生からハンドラ実行までの流れ
C# eventの流れは、次の順番で考えると分かりやすいです。
イベントを宣言する
イベントハンドラを用意する
+=でイベントハンドラを登録するクラス内でイベントを発生させる
登録されたイベントハンドラが実行される
コード全体で見ると、次のようになります。
C#public class Worker
{
public event Action? Completed;
public void DoWork()
{
Console.WriteLine("処理を実行中...");
Completed?.Invoke();
}
}
public class Program
{
public static void Main()
{
var worker = new Worker();
worker.Completed += OnWorkerCompleted;
worker.DoWork();
}
private static void OnWorkerCompleted()
{
Console.WriteLine("イベントを受け取りました");
}
}
worker.DoWork()を呼び出すと、Workerクラスの中でCompletedイベントが発生し、登録されていたOnWorkerCompletedが実行されます。
4-4. 複数のイベントハンドラを登録した場合の動き
eventには複数のイベントハンドラを登録できます。
C#var worker = new Worker();
worker.Completed += () => Console.WriteLine("ログを出力しました");
worker.Completed += () => Console.WriteLine("メールを送信しました");
worker.Completed += () => Console.WriteLine("画面を更新しました");
worker.DoWork();
実行結果は次のようになります。
処理を実行中...
ログを出力しました
メールを送信しました
画面を更新しました
複数のイベントハンドラを登録した場合、基本的には登録した順番に実行されます。
ただし、イベントハンドラの実行順序に強く依存する設計は避けたほうがよいです。
イベントは「通知」であり、「必ずこの順番で処理を進めるための制御構文」ではありません。
4-5. イベント解除を忘れたときに起きる問題
eventは便利ですが、登録解除を忘れると問題になることがあります。
特に、長く生き続けるオブジェクトのイベントを、短命なオブジェクトが購読する場合は注意が必要です。
C#publisher.SomeEvent += subscriber.HandleEvent;
この場合、publisherはsubscriber.HandleEventへの参照を保持します。
そのため、subscriberが不要になっても、イベントに登録されたままだとガベージコレクションの対象にならないことがあります。
使い終わったら、必要に応じて-=で解除します。
C#publisher.SomeEvent -= subscriber.HandleEvent;
特に、static eventやアプリケーション全体で長く存在するサービスのeventを購読する場合は、解除忘れによるメモリリークに注意しましょう。
5. EventHandlerとは?eventとの違いを初心者向けに解説
5-1. EventHandlerはイベント用に用意された標準delegate
EventHandlerは、.NETでイベント用に用意されている標準delegateです。
次のような形で使います。
C#public event EventHandler? Completed;
EventHandlerを使うと、イベントハンドラは次の形になります。
C#void Handler(object? sender, EventArgs e)
たとえば、次のように書きます。
C#private static void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine("処理が完了しました");
}
C# eventでは、自作delegateを使うこともできますが、一般的なイベントではEventHandlerを使うことが多いです。
5-2. eventは仕組み、EventHandlerは型という違い
初心者が混乱しやすいのが、eventとEventHandlerの違いです。
この2つは同じものではありません。
| 用語 | 意味 |
|---|---|
| event | イベントとして公開するための仕組み |
| EventHandler | イベントハンドラの形を表すdelegate型 |
つまり、eventは「仕組み」であり、EventHandlerは「型」です。
次のコードで見ると分かりやすいです。
C#public event EventHandler? Completed;
このコードでは、Completedというイベントを宣言しています。
event:イベントとして公開するためのキーワードEventHandler:イベントに登録できるメソッドの型Completed:イベント名
つまり、EventHandlerだけではeventにはなりません。
C#public EventHandler? Completed;
これは単なるdelegate型のフィールドです。
イベントとして安全に公開するには、eventキーワードを付けます。
C#public event EventHandler? Completed;
5-3. EventHandlerの基本形「object sender, EventArgs e」
EventHandlerの基本形は次のとおりです。
C#void Handler(object? sender, EventArgs e)
引数は2つあります。
| 引数 | 意味 |
|---|---|
| sender | イベントを発生させたオブジェクト |
| e | イベントに関する追加情報 |
たとえば、次のようなイベントハンドラを作れます。
C#private static void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine("Completedイベントを受け取りました");
}
この形は、.NETのイベント実装で広く使われている標準的なパターンです。
5-4. senderには何が入るのか
senderには、通常、イベントを発生させたオブジェクト自身を渡します。
C#Completed?.Invoke(this, EventArgs.Empty);
このthisがsenderになります。
たとえば、次のように使えます。
C#private static void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine(sender?.GetType().Name);
}
Workerクラスがイベントを発生させた場合、senderにはWorkerインスタンスが入ります。
C#public class Worker
{
public event EventHandler? Completed;
public void DoWork()
{
Completed?.Invoke(this, EventArgs.Empty);
}
}
senderを使うと、「どのオブジェクトから発生したイベントなのか」をイベントハンドラ側で判断できます。
5-5. EventArgsには何を入れるのか
EventArgsは、イベントに関する追加情報を渡すためのオブジェクトです。
追加情報がない場合は、EventArgs.Emptyを使います。
C#Completed?.Invoke(this, EventArgs.Empty);
たとえば、単に「処理が完了した」と通知するだけなら、追加データは不要です。
一方で、処理結果やメッセージなどを渡したい場合は、独自のEventArgsクラスを作ります。
C#public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public WorkCompletedEventArgs(string message)
{
Message = message;
}
}
このように、EventArgsはイベント発生時のデータを入れるために使います。
5-6. 自作delegateとEventHandlerはどちらを使うべきか
初心者が迷いやすいのが、自作delegateとEventHandlerの使い分けです。
基本的には、通常のイベントであればEventHandlerまたはEventHandler<TEventArgs>を使うのがおすすめです。
C#public event EventHandler? Completed;
データを渡したい場合は、EventHandler<TEventArgs>を使います。
C#public event EventHandler<WorkCompletedEventArgs>? Completed;
自作delegateは、特殊な引数や戻り値が必要な場合に使えます。
ただし、イベントは通常、戻り値を持たない通知として設計します。
そのため、C# eventを一般的な.NETの作法に合わせて実装するなら、EventHandlerを使うとよいでしょう。
6. EventHandlerを使ったC# eventの実装サンプル
6-1. EventHandlerでイベントを宣言する
EventHandlerを使ったeventは、次のように宣言します。
C#public class Worker
{
public event EventHandler? Completed;
}
Completedイベントには、次の形のメソッドを登録できます。
C#void Handler(object? sender, EventArgs e)
6-2. EventHandlerに対応したイベントハンドラを書く
実際にイベントハンドラを書くと、次のようになります。
C#private static void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine("処理が完了しました");
}
そして、+=で登録します。
C#var worker = new Worker();
worker.Completed += OnCompleted;
EventHandlerを使う場合、引数の形が一致していないとコンパイルエラーになります。
たとえば、次のようなメソッドは登録できません。
C#private static void OnCompleted()
{
Console.WriteLine("処理が完了しました");
}
EventHandlerに登録するには、必ずobject? sender, EventArgs eの形に合わせます。
6-3. イベントを発生させるOn〇〇メソッドの作り方
.NETのイベント実装では、イベントを発生させる処理をOnイベント名というメソッドに分けることがよくあります。
C#public class Worker
{
public event EventHandler? Completed;
public void DoWork()
{
Console.WriteLine("処理を実行中...");
OnCompleted(EventArgs.Empty);
}
protected virtual void OnCompleted(EventArgs e)
{
Completed?.Invoke(this, e);
}
}
このOnCompletedメソッドは、Completedイベントを発生させるためのメソッドです。
protected virtualにしておくと、継承先のクラスでイベント発生時の処理を拡張できます。
C#protected override void OnCompleted(EventArgs e)
{
Console.WriteLine("派生クラス側の処理");
base.OnCompleted(e);
}
必ずしもすべてのクラスでこの形にする必要はありませんが、公開APIや再利用性の高いクラスではよく使われるパターンです。
6-4. EventArgs.Emptyを使うケース
イベントで追加情報を渡す必要がない場合は、EventArgs.Emptyを使います。
C#Completed?.Invoke(this, EventArgs.Empty);
たとえば、単に「処理が完了した」と通知するだけなら、追加データは不要です。
C#public class Worker
{
public event EventHandler? Completed;
public void DoWork()
{
Console.WriteLine("処理中...");
Completed?.Invoke(this, EventArgs.Empty);
}
}
受け取る側も、EventArgsの中身を使わないことがあります。
C#private static void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine("完了通知を受け取りました");
}
このようなケースでは、EventArgs.Emptyを使うのが自然です。
6-5. .NETで推奨されるイベント実装パターン
.NETでよく使われるイベント実装パターンは、次の形です。
C#public class Worker
{
public event EventHandler? Completed;
public void DoWork()
{
Console.WriteLine("処理中...");
OnCompleted(EventArgs.Empty);
}
protected virtual void OnCompleted(EventArgs e)
{
Completed?.Invoke(this, e);
}
}
使う側は次のようになります。
C#public class Program
{
public static void Main()
{
var worker = new Worker();
worker.Completed += Worker_Completed;
worker.DoWork();
}
private static void Worker_Completed(object? sender, EventArgs e)
{
Console.WriteLine("イベントを受け取りました");
}
}
この形に慣れておくと、Windows Forms、WPF、ASP.NETなどのイベントコードも読みやすくなります。
7. EventHandler<TEventArgs>と独自EventArgsの使い方
7-1. イベント発生時にデータを渡したい場合
イベント発生時に、単なる通知だけでなくデータを渡したい場合があります。
たとえば、次のような情報を渡したいケースです。
処理結果
メッセージ
エラー内容
進捗率
更新された値
このような場合は、EventHandler<TEventArgs>を使います。
C#public event EventHandler<WorkCompletedEventArgs>? Completed;
TEventArgsには、イベントで渡したいデータを持つクラスを指定します。
7-2. 独自EventArgsクラスを作成する
イベントで渡すデータがある場合は、独自のEventArgsクラスを作ります。
C#public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public bool Success { get; }
public WorkCompletedEventArgs(string message, bool success)
{
Message = message;
Success = success;
}
}
このクラスには、イベントハンドラ側に渡したい情報をプロパティとして定義します。
ここでは、メッセージと成功フラグを持たせています。
7-3. EventHandler<TEventArgs>でイベントを宣言する
独自EventArgsを作成したら、EventHandler<TEventArgs>でイベントを宣言します。
C#public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? Completed;
public void DoWork()
{
Console.WriteLine("処理中...");
var args = new WorkCompletedEventArgs("処理が正常に完了しました", true);
Completed?.Invoke(this, args);
}
}
このようにすると、イベント発生時にデータを渡せます。
7-4. メッセージや結果データを渡すサンプルコード
全体のサンプルコードを見てみましょう。
C#public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public bool Success { get; }
public WorkCompletedEventArgs(string message, bool success)
{
Message = message;
Success = success;
}
}
public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? Completed;
public void DoWork()
{
Console.WriteLine("処理を開始します");
bool success = true;
string message = "処理が正常に完了しました";
OnCompleted(new WorkCompletedEventArgs(message, success));
}
protected virtual void OnCompleted(WorkCompletedEventArgs e)
{
Completed?.Invoke(this, e);
}
}
public class Program
{
public static void Main()
{
var worker = new Worker();
worker.Completed += Worker_Completed;
worker.DoWork();
}
private static void Worker_Completed(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine($"メッセージ: {e.Message}");
Console.WriteLine($"成功: {e.Success}");
}
}
実行結果は次のようになります。
処理を開始します
メッセージ: 処理が正常に完了しました
成功: True
このように、EventHandler<TEventArgs>を使うと、イベント発生時に必要な情報を安全に渡せます。
7-5. EventArgsを継承すべきケースと不要なケース
.NETのイベントパターンに沿う場合、イベントデータ用のクラスはEventArgsを継承するのが一般的です。
C#public class ValueChangedEventArgs : EventArgs
{
public int OldValue { get; }
public int NewValue { get; }
public ValueChangedEventArgs(int oldValue, int newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}
一方で、クラス内部だけで使う簡単なコールバックであれば、必ずしもEventArgsを作る必要はありません。
C#public event Action<int>? ProgressChanged;
このように、進捗率だけを簡単に通知したい場合はAction<int>でも十分なことがあります。
ただし、公開APIとしてeventを設計する場合や、.NETらしいイベント設計にしたい場合は、EventHandler<TEventArgs>と独自EventArgsを使うのが分かりやすく、拡張もしやすいです。
8. C# eventでよくあるエラーと対処法
8-1. 「eventは外部から直接呼び出せない」と言われる理由
次のようなコードを書くと、コンパイルエラーになります。
C#worker.Completed?.Invoke(this, EventArgs.Empty);
Completedがeventとして宣言されている場合、外部から直接呼び出すことはできません。
C#public class Worker
{
public event EventHandler? Completed;
}
eventは、外部からは+=と-=による登録・解除だけが許可されます。
イベントを発生させられるのは、基本的にそのeventを宣言しているクラスの内部だけです。
正しくは、クラス内にイベントを発生させるメソッドを用意します。
C#public class Worker
{
public event EventHandler? Completed;
public void DoWork()
{
Completed?.Invoke(this, EventArgs.Empty);
}
}
外部からは、イベントを直接呼び出すのではなく、イベントが発生するきっかけとなるメソッドを呼び出します。
C#worker.DoWork();
8-2. イベントハンドラの引数が一致しないエラー
EventHandlerを使っている場合、登録するメソッドは次の形である必要があります。
C#void Handler(object? sender, EventArgs e)
そのため、次のようなメソッドは登録できません。
C#private void OnCompleted()
{
Console.WriteLine("完了");
}
次のように修正します。
C#private void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine("完了");
}
EventHandler<TEventArgs>の場合は、第2引数の型も一致させる必要があります。
C#public event EventHandler<WorkCompletedEventArgs>? Completed;
この場合、イベントハンドラは次の形です。
C#private void OnCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine(e.Message);
}
eventでエラーが出たときは、まず「イベントの型」と「登録しているメソッドの引数」が一致しているか確認しましょう。
8-3. eventがnullで実行時エラーになるケース
イベントハンドラが1つも登録されていない状態でイベントを直接呼び出すと、NullReferenceExceptionが発生する可能性があります。
C#Completed(this, EventArgs.Empty);
安全に呼び出すには、次のようにnullチェックします。
C#if (Completed != null)
{
Completed(this, EventArgs.Empty);
}
または、より簡潔に次のように書けます。
C#Completed?.Invoke(this, EventArgs.Empty);
現在はこの?.Invoke()の形がよく使われます。
C#protected virtual void OnCompleted(EventArgs e)
{
Completed?.Invoke(this, e);
}
イベントを発生させるときは、登録されているハンドラがない場合も考えて、安全に呼び出しましょう。
8-4. static eventで起きやすい注意点
static eventは、アプリケーション全体で共有されるイベントです。
C#public static event EventHandler? GlobalChanged;
便利に見えますが、注意が必要です。
static eventは長く生き続けるため、イベントを購読したオブジェクトへの参照も残りやすくなります。
C#GlobalNotifier.GlobalChanged += instance.HandleChanged;
この状態で解除を忘れると、instanceが不要になっても参照され続け、メモリリークの原因になることがあります。
使い終わったら、適切なタイミングで解除します。
C#GlobalNotifier.GlobalChanged -= instance.HandleChanged;
static eventは便利ですが、安易に使うと依存関係が見えにくくなります。
必要な場面に限定して使うのがよいでしょう。
8-5. 購読解除忘れによるメモリリークの注意点
eventでは、発行側が購読側のイベントハンドラを参照します。
そのため、発行側が長く生き続ける場合、購読側が不要になっても解放されないことがあります。
C#publisher.SomeEvent += subscriber.HandleEvent;
この関係では、publisherがsubscriberへの参照を持つことになります。
解除するには、次のようにします。
C#publisher.SomeEvent -= subscriber.HandleEvent;
特に注意が必要なのは、次のようなケースです。
static eventを購読する
シングルトンのサービスのeventを購読する
長寿命の監視オブジェクトのeventを購読する
UI画面が閉じられてもeventを解除していない
一時的に使う画面やオブジェクトが長寿命イベントを購読する場合は、破棄時や終了時に解除する設計を考えましょう。
9. C# eventを使うべき場面・使わない方がよい場面
9-1. UI操作や状態変化の通知に向いている
C# eventは、UI操作や状態変化の通知に向いています。
代表的な例は、ボタンクリックです。
C#button.Click += Button_Click;
また、独自クラスでも、状態が変わったことを通知する場面に向いています。
C#public class Counter
{
public event EventHandler? ValueChanged;
private int _value;
public int Value
{
get => _value;
set
{
if (_value == value)
{
return;
}
_value = value;
ValueChanged?.Invoke(this, EventArgs.Empty);
}
}
}
このように、「値が変わった」「操作された」「処理が完了した」といった通知にはeventが適しています。
9-2. クラス間の依存を減らしたいときに有効
eventは、クラス間の依存を減らしたいときにも有効です。
たとえば、Workerクラスが処理完了後に何をするかを知らなくても、eventで通知だけできます。
C#public class Worker
{
public event EventHandler? Completed;
public void DoWork()
{
Console.WriteLine("処理中...");
Completed?.Invoke(this, EventArgs.Empty);
}
}
使う側は、必要な処理を自由に登録できます。
C#worker.Completed += (sender, e) => Console.WriteLine("ログを出力");
worker.Completed += (sender, e) => Console.WriteLine("画面を更新");
Workerクラスはログ出力や画面更新のことを知る必要がありません。
このように、eventは「何かが起きたことだけを通知し、その後の処理は外部に任せる」設計に向いています。
9-3. 単純なメソッド呼び出しで十分なケース
すべての場面でeventを使う必要はありません。
たとえば、処理の流れが明確で、呼び出す相手も決まっている場合は、普通のメソッド呼び出しで十分です。
C#logger.Write("処理が完了しました");
eventを使うと、処理の流れが柔軟になる一方で、どこで何が実行されるのか追いにくくなることもあります。
次のような場合は、eventを使わないほうが分かりやすいことがあります。
呼び出す処理が1つだけ
呼び出し先が明確に決まっている
処理順序を厳密に制御したい
戻り値を使って次の処理を決めたい
eventは通知には向いていますが、通常の処理呼び出しをすべて置き換えるものではありません。
9-4. ActionやFuncとの使い分け
C#では、event以外にもActionやFuncを使って処理を渡せます。
C#public Action? Completed;
または、コールバックとしてメソッドに渡すこともできます。
C#public void DoWork(Action onCompleted)
{
Console.WriteLine("処理中...");
onCompleted();
}
Actionは戻り値のない処理、Funcは戻り値のある処理を表します。
C#Func<int, int, int> add = (x, y) => x + y;
eventとAction/Funcの使い分けは、次のように考えると分かりやすいです。
| 使いたいもの | 向いている場面 |
|---|---|
| event | 複数の相手に出来事を通知したい |
| Action | 簡単なコールバックを渡したい |
| Func | 結果を返す処理を渡したい |
| EventHandler | .NET標準のイベント形式にしたい |
公開するイベントとして設計するなら、event EventHandlerが自然です。
一方、メソッド内部の一時的なコールバックであれば、ActionやFuncで十分な場合もあります。
9-5. Observerパターンとの関係
C# eventは、デザインパターンのObserverパターンと関係があります。
Observerパターンは、あるオブジェクトの状態変化を、複数のオブジェクトに通知するための設計パターンです。
C# eventでは、イベントを発生させる側が「通知元」、イベントハンドラを登録する側が「通知先」になります。
C#publisher.SomeEvent += subscriber.HandleEvent;
この構造は、Observerパターンの考え方に近いです。
ただし、C# eventは言語機能として用意されているため、Observerパターンを比較的簡単に実装できます。
「あるクラスの変化を複数のクラスに知らせたい」という場面では、eventは非常に便利な選択肢です。
まとめ
C#のeventは、「何かが起きたことを通知する」ための仕組みです。
ボタンクリック、処理完了、状態変更、エラー通知など、イベント駆動のプログラムでよく使われます。
eventはdelegateをベースにしていますが、delegateをそのまま公開する場合と違い、外部からできる操作を+=と-=に制限できます。これにより、外部から勝手にイベントを呼び出したり、登録済みのハンドラをすべて上書きしたりすることを防げます。
基本的なeventの書き方は次のとおりです。
C#public event EventHandler? Completed;
イベントを発生させるときは、クラス内部で次のように呼び出します。
C#Completed?.Invoke(this, EventArgs.Empty);
追加データを渡したい場合は、独自のEventArgsクラスを作り、EventHandler<TEventArgs>を使います。
C#public event EventHandler<WorkCompletedEventArgs>? Completed;
eventとEventHandlerの違いは、次のように整理できます。
| 用語 | 意味 |
|---|---|
| event | イベントとして公開するための仕組み |
| EventHandler | イベントハンドラの標準delegate型 |
| EventHandler<TEventArgs> | データ付きイベントに使う標準delegate型 |
C# eventを使うと、クラス同士を疎結合にしながら、処理完了や状態変化を柔軟に通知できます。
一方で、単純なメソッド呼び出しで十分な場面や、イベント解除を忘れると問題になる場面もあります。
初心者のうちは、まず次の3点を押さえると理解しやすくなります。
eventは「何かが起きたことを通知する」仕組み
+=でイベントハンドラを登録し、-=で解除するEventHandlerは.NETでよく使われるイベント用の標準delegate
この基本を理解できれば、C# eventのコードを読む力も、自分で実装する力も大きく伸びます。

