C#のeventをInvokeする方法とは?Nullチェック・発火タイミング・使い方を初心者向けに解説
はじめに
C#のeventは、クラス内で発生した出来事を外部へ通知するための仕組みです。たとえば、処理が完了したとき、データが変更されたとき、エラーが発生したときなどにイベントを発火し、登録されているイベントハンドラーを実行できます。
イベントを発火するときに使われるのが、デリゲートのInvokeメソッドです。
Completed?.Invoke(this, EventArgs.Empty);このコードでは、Completedイベントに購読者がいる場合だけ、登録されているイベントハンドラーを呼び出します。
本記事では、C#のeventをInvokeする基本的な方法から、Nullチェック、発火タイミング、EventHandlerの使い方、非同期処理や複数スレッドでの注意点まで、初心者向けに解説します。
1. C#のeventとInvokeの基本
1-1. eventとは何か
C#のeventは、あるオブジェクトで発生した出来事を、別のオブジェクトへ通知するための機能です。
たとえば、次のような出来事をイベントとして表現できます。
ボタンがクリックされた
データが変更された
ファイルの読み込みが完了した
処理中にエラーが発生した
ユーザーがログインした
イベントを利用すると、通知する側は、通知を受け取る側の具体的な処理を知る必要がありません。
public event EventHandler? Completed;この例では、Completedという名前のイベントを宣言しています。EventHandlerは、C#で一般的に使われるイベント用のデリゲートです。
イベントを利用する処理は、主に次の2つに分かれます。
イベントを発行する側が
eventを宣言して発火するイベントを受け取る側がイベントハンドラーを登録する
このように処理を分離できる点が、イベントを使う大きなメリットです。
1-2. Invokeとは何をするメソッドなのか
Invokeは、デリゲートに登録されているメソッドを呼び出すためのメソッドです。
イベントはデリゲートを基に作られているため、イベントを発火するときにもInvokeを使用できます。
Completed?.Invoke(this, EventArgs.Empty);このコードが実行されると、Completedイベントに登録されているすべてのイベントハンドラーが呼び出されます。
EventHandlerの一般的な形式は次のとおりです。
void Handler(object? sender, EventArgs e){}そのため、Invokeにも次の2つの引数を渡します。
Completed?.Invoke(sender, eventArgs);第1引数:イベントの発生元を表す
sender第2引数:イベントに関連する情報を表す
EventArgs
イベントを宣言したオブジェクト自身が発生元である場合、第1引数には通常thisを渡します。
1-3. delegateとeventの違い
delegateは、メソッドを変数のように保持して呼び出せる仕組みです。
public delegate void MessageHandler(string message);public MessageHandler? MessageReceived;
この場合、外部クラスから次のような操作が可能です。
publisher.MessageReceived = Handler;publisher.MessageReceived?.Invoke("メッセージ");外部から代入やInvokeができるため、意図しない上書きや発火が行われる可能性があります。
一方、eventを付けると、外部クラスが行える操作を購読と購読解除に制限できます。
public event MessageHandler? MessageReceived;外部クラスから許可されるのは、基本的に次の操作だけです。
publisher.MessageReceived += Handler;publisher.MessageReceived -= Handler;外部から直接代入したり、Invokeしたりすることはできません。
つまり、delegateとeventには次の違いがあります。
| 項目 | delegate | event |
|---|---|---|
| メソッドの登録 | 可能 | 可能 |
| メソッドの解除 | 可能 | 可能 |
| 外部からの上書き | 可能 | 不可 |
| 外部からのInvoke | 可能 | 不可 |
| 主な用途 | コールバックなど | 出来事の通知 |
イベントとして公開する場合は、通常のデリゲートフィールドではなく、eventを使用するのが基本です。
1-4. イベントを発行する側と受け取る側の役割
イベントを発行する側は、次の処理を担当します。
イベントを宣言する
イベントを発火する条件を決める
必要なデータをイベント引数に設定する
適切なタイミングで
Invokeする
イベントを受け取る側は、次の処理を担当します。
イベントハンドラーを作成する
+=でイベントを購読する通知を受け取ったときの処理を書く
不要になったら
-=で購読を解除する
基本的な関係は次のようになります。
public class Worker{public event EventHandler? Completed;public void Execute(){Console.WriteLine("処理を実行します。");Completed?.Invoke(this, EventArgs.Empty);}
}
public class Program{public static void Main(){var worker = new Worker();
worker.Completed += OnCompleted;worker.Execute();}private static void OnCompleted(object? sender, EventArgs e){Console.WriteLine("完了通知を受け取りました。");}
}
Workerは処理完了を通知するだけであり、通知を受け取った側が何をするかまでは知りません。この分離によって、クラス同士の依存関係を弱くできます。
2. C#のeventをInvokeする基本的な方法
2-1. イベントを宣言する基本構文
イベントは、eventキーワードとデリゲート型を使って宣言します。
public event EventHandler? Completed;基本構文は次のとおりです。
アクセス修飾子 event デリゲート型? イベント名;EventHandlerを使わず、独自デリゲートを宣言することもできます。
public delegate void MessageEventHandler(string message);public event MessageEventHandler? MessageReceived;
ただし、一般的な.NETの設計では、可能な限りEventHandlerまたはEventHandler<TEventArgs>を使用すると、イベントの形式を統一できます。
2-2. イベントハンドラーを登録する方法
イベントハンドラーは、+=演算子を使って登録します。
worker.Completed += OnCompleted;登録するメソッドの引数と戻り値は、イベントのデリゲート型と一致している必要があります。
private static void OnCompleted(object? sender, EventArgs e){Console.WriteLine("処理が完了しました。");}ラムダ式を使って登録することもできます。
worker.Completed += (sender, e) =>{Console.WriteLine("ラムダ式で完了通知を受け取りました。");};複数のイベントハンドラーを登録した場合、イベントが発火すると、登録されているハンドラーが順番に呼び出されます。
worker.Completed += FirstHandler;worker.Completed += SecondHandler;2-3. Invokeでイベントを発火する方法
イベントを発火する基本的なコードは次のとおりです。
Completed?.Invoke(this, EventArgs.Empty);?.はNull条件演算子です。Completedがnullではない場合だけ、Invokeを実行します。
従来のif文で書くと、次のコードとほぼ同じ意味になります。
if (Completed != null){Completed.Invoke(this, EventArgs.Empty);}Invokeを省略し、デリゲートを直接呼び出す書き方もあります。
Completed?.Invoke(this, EventArgs.Empty);イベントの発火処理は、一般的にOnイベント名というprotected virtualメソッドへ集約します。
protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}処理本体からは、次のように呼び出します。
public void Execute(){// メイン処理OnCompleted();}2-4. EventHandlerを使った実装例
EventHandlerを使った基本的な実装例は次のとおりです。
public class FileLoader{public event EventHandler? LoadCompleted;public void Load(){Console.WriteLine("ファイルを読み込んでいます。");// 読み込み処理を想定Thread.Sleep(500);OnLoadCompleted();}protected virtual void OnLoadCompleted(){LoadCompleted?.Invoke(this, EventArgs.Empty);}
}
public class Program{public static void Main(){var loader = new FileLoader();
loader.LoadCompleted += HandleLoadCompleted;loader.Load();}private static void HandleLoadCompleted(object? sender, EventArgs e){Console.WriteLine("ファイルの読み込みが完了しました。");}
}
実行結果は次のようになります。
ファイルを読み込んでいます。ファイルの読み込みが完了しました。追加データを渡す必要がないイベントでは、EventHandlerとEventArgs.Emptyを使用できます。
2-5. 独自デリゲートをInvokeする実装例
独自デリゲートを使用すると、イベントハンドラーの引数を自由に定義できます。
public delegate void ProgressChangedHandler(int progress);public class Downloader{public event ProgressChangedHandler? ProgressChanged;
public void Download(){for (int progress = 0; progress <= 100; progress += 20){ProgressChanged?.Invoke(progress);}}
}
public class Program{public static void Main(){var downloader = new Downloader();
downloader.ProgressChanged += progress =>{Console.WriteLine($"進捗率: {progress}%");};downloader.Download();}
}
この例では、イベントの購読者へ進捗率を直接渡しています。
ただし、.NETで一般的なイベント形式に合わせるなら、次のようにEventHandler<TEventArgs>を使用する設計も検討しましょう。
public event EventHandler<ProgressChangedEventArgs>? ProgressChanged;3. eventのInvokeでNullチェックが必要な理由
3-1. 購読者がいないイベントはnullになる
イベントを宣言した直後は、イベントハンドラーが1つも登録されていません。
public event EventHandler? Completed;この状態では、Completedの値はnullです。
イベントハンドラーが登録されると、呼び出す対象がイベントに追加されます。
worker.Completed += OnCompleted;最後のイベントハンドラーが解除されると、再びnullになることがあります。
worker.Completed -= OnCompleted;そのため、イベントを発火するときは、購読者が存在しない可能性を考慮する必要があります。
3-2. NullReferenceExceptionが発生するケース
購読者がいないイベントに対して、NullチェックをせずにInvokeすると、NullReferenceExceptionが発生します。
public class Worker{public event EventHandler? Completed;public void Execute(){Completed.Invoke(this, EventArgs.Empty);}
}
Completedがnullであるため、Invokeメソッドを呼び出せません。
Null許容参照型が有効な環境では、コンパイラーから警告が表示される場合もあります。
null 参照の逆参照である可能性があります警告を無視するために!を付ける方法もありますが、購読者の存在が保証されていない場合には適切ではありません。
Completed!.Invoke(this, EventArgs.Empty);この書き方ではコンパイラー警告を抑制できても、実行時例外を防げません。
3-3. if文でNullチェックしてからInvokeする方法
if文を使う場合は、次のように記述します。
if (Completed != null){Completed.Invoke(this, EventArgs.Empty);}コードの意図が明確であるため、初心者にも理解しやすい書き方です。
複数スレッドから購読や購読解除が行われる可能性がある場合は、ローカル変数へコピーしてからチェックする書き方もあります。
EventHandler? handler = Completed;if (handler != null){handler.Invoke(this, EventArgs.Empty);}
イベントをローカル変数へコピーすることで、Nullチェック後に元のイベントから購読者が解除されても、コピーしたデリゲートを呼び出せます。
3-4. ?.Invokeで安全にイベントを発火する方法
現在のC#では、Null条件演算子を使った次の書き方が一般的です。
Completed?.Invoke(this, EventArgs.Empty);Completedがnullの場合、Invokeは実行されません。例外も発生せず、そのまま次の処理へ進みます。
Completedがnullではない場合だけ、登録されているイベントハンドラーが呼び出されます。
独自デリゲートの場合も同様です。
MessageReceived?.Invoke("新しいメッセージです。");?.Invokeは簡潔であり、イベント発火の定番パターンとして広く使われています。
3-5. if文と?.Invokeのどちらを使うべきか
通常は、次の?.Invokeを使用するとよいでしょう。
Completed?.Invoke(this, EventArgs.Empty);コードが短く、Nullチェックと呼び出しを安全に記述できます。
一方、呼び出し前後に別の処理が必要な場合は、if文のほうが読みやすいことがあります。
if (Completed != null){Console.WriteLine("購読者へ完了を通知します。");Completed.Invoke(this, EventArgs.Empty);}基本的な使い分けは次のとおりです。
単純に安全な発火を行う:
?.Invoke購読者がいる場合だけ追加処理を行う:if文
デリゲートを個別に処理したい:ローカル変数や
GetInvocationListを使用する
特別な理由がなければ、簡潔な?.Invokeを選ぶのがおすすめです。
4. eventをInvokeするタイミング
4-1. 処理の開始時にイベントを発火するケース
長時間かかる処理を開始するときにイベントを発火すると、画面に「処理中」と表示したり、操作ボタンを無効化したりできます。
public event EventHandler? ProcessingStarted;public void Execute(){ProcessingStarted?.Invoke(this, EventArgs.Empty);
// メイン処理
}
より一般的には、発火処理を専用メソッドへ分離します。
protected virtual void OnProcessingStarted(){ProcessingStarted?.Invoke(this, EventArgs.Empty);}ただし、開始イベントは、実際に処理を開始できることが確定したタイミングで発火する必要があります。入力チェックの前に発火すると、処理が始まっていないのに開始通知だけが送られる可能性があります。
public void Execute(string filePath){ArgumentException.ThrowIfNullOrWhiteSpace(filePath);OnProcessingStarted();// メイン処理
}
4-2. 処理の完了後にイベントを発火するケース
処理完了イベントは、対象の処理が正常に終わった後で発火します。
public event EventHandler? Completed;public void Execute(){PerformMainProcess();Completed?.Invoke(this, EventArgs.Empty);}
処理の途中で発火すると、購読者が未完成のデータを参照する可能性があります。
public void Save(){Validate();WriteFile();UpdateState();OnCompleted();
}
処理が失敗した場合にも完了イベントを発火するのか、正常終了時だけ発火するのかを明確にしておくことが重要です。
正常終了だけを表す場合はCompleted、成功・失敗を含む終了を表す場合はFinishedなど、意味に合ったイベント名を検討します。
4-3. 値や状態が変更されたときに発火するケース
プロパティの値が変わったことを通知するイベントもよく使われます。
public class Counter{private int _value;public event EventHandler? ValueChanged;public int Value{get => _value;set{if (_value == value){return;}_value = value;ValueChanged?.Invoke(this, EventArgs.Empty);}}
}
変更前と同じ値が設定された場合に発火する必要がなければ、値を比較してからイベントを発火します。
変更後の値を購読者へ渡す場合は、独自のEventArgsを使用できます。
public class ValueChangedEventArgs : EventArgs{public ValueChangedEventArgs(int oldValue, int newValue){OldValue = oldValue;NewValue = newValue;}public int OldValue { get; }public int NewValue { get; }
}
4-4. エラー発生時にイベントを発火するケース
処理中のエラーを通知するために、エラーイベントを用意することもできます。
public event EventHandler<ErrorEventArgs>? ErrorOccurred;public void Execute(){try{PerformMainProcess();}catch (Exception ex){ErrorOccurred?.Invoke(this, new ErrorEventArgs(ex));}}
独自のイベント引数は次のように定義できます。
public class ErrorEventArgs : EventArgs{public ErrorEventArgs(Exception exception){Exception = exception;}public Exception Exception { get; }
}
ただし、イベントを発火した後に例外を握りつぶすべきか、再スローすべきかはアプリケーションの設計によって異なります。
catch (Exception ex){OnErrorOccurred(new ErrorEventArgs(ex));throw;}エラーイベントは例外処理の代替ではありません。呼び出し元にも失敗を知らせる必要がある場合は、例外を適切に伝える設計にします。
4-5. コンストラクター内でInvokeする際の注意点
コンストラクター内でイベントを発火しても、通常は外部からイベントハンドラーを登録する前です。
var service = new Service();service.Initialized += OnInitialized;このコードでは、Serviceのコンストラクターが完了した後にInitializedイベントを購読しています。そのため、コンストラクター内で発火したイベントは受け取れません。
public Service(){Initialized?.Invoke(this, EventArgs.Empty);}また、基底クラスのコンストラクターから仮想メソッドを呼び出すと、派生クラスの初期化が完了していない状態で処理が実行される危険があります。
初期化完了を通知したい場合は、コンストラクターとは別に初期化メソッドを用意する方法があります。
public void Initialize(){// 初期化処理OnInitialized();}非同期初期化が必要な場合は、InitializeAsyncなどのメソッドを用意するとよいでしょう。
4-6. ループ内で何度もInvokeする際の注意点
進捗通知などでは、ループ内でイベントを何度も発火することがあります。
for (int i = 0; i < items.Count; i++){Process(items[i]);ProgressChanged?.Invoke(this,new ProgressChangedEventArgs(i + 1, items.Count));
}
イベントの発火回数が多すぎると、パフォーマンスが低下する可能性があります。特に、購読者が画面更新やファイルアクセスなどの重い処理を行う場合は注意が必要です。
一定件数ごとに通知する方法もあります。
for (int i = 0; i < items.Count; i++){Process(items[i]);if ((i + 1) % 100 == 0 || i == items.Count - 1){OnProgressChanged(i + 1, items.Count);}
}
発火頻度は、必要な通知精度と処理負荷のバランスを考えて決めましょう。
5. EventHandlerとEventArgsを使った実践的な使い方
5-1. EventHandlerを使うメリット
EventHandlerは、.NETで標準的に使われるイベント用デリゲートです。
定義は、概念的には次の形式です。
public delegate void EventHandler(object? sender, EventArgs e);EventHandlerを使うメリットには、次のようなものがあります。
イベントの形式を統一できる
発生元を
senderで渡せるイベントデータを
EventArgsで渡せる.NETの一般的な設計に合わせられる
独自デリゲートを毎回定義する必要がない
データを渡さないイベントでは、次のように宣言します。
public event EventHandler? Completed;独自データを渡す場合は、EventHandler<TEventArgs>を使用します。
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;5-2. EventArgs.Emptyを渡してInvokeする方法
イベント固有のデータがない場合は、EventArgs.Emptyを渡します。
Completed?.Invoke(this, EventArgs.Empty);毎回new EventArgs()を作る必要はありません。
Completed?.Invoke(this, new EventArgs());上記でも動作しますが、追加データがないことを明確にするため、通常はEventArgs.Emptyを使用します。
発火処理をメソッドへ分離すると、次のようになります。
protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}5-3. 独自のEventArgsクラスを作成する方法
イベントと一緒にデータを渡したい場合は、EventArgsを継承したクラスを作成します。
public class MessageReceivedEventArgs : EventArgs{public MessageReceivedEventArgs(string message, DateTime receivedAt){Message = message;ReceivedAt = receivedAt;}public string Message { get; }public DateTime ReceivedAt { get; }
}
イベントは、EventHandler<TEventArgs>を使って宣言します。
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;発火時に独自のイベント引数を作成します。
protected virtual void OnMessageReceived(string message){var eventArgs = new MessageReceivedEventArgs(message,DateTime.Now);MessageReceived?.Invoke(this, eventArgs);
}
購読者は、イベント引数からデータを取得できます。
private static void OnMessageReceived(object? sender,MessageReceivedEventArgs e){Console.WriteLine($"受信時刻: {e.ReceivedAt}");Console.WriteLine($"内容: {e.Message}");}5-4. イベント発生元をsenderで渡す方法
senderには、イベントを発生させたオブジェクトを渡します。通常はthisです。
StatusChanged?.Invoke(this, eventArgs);購読者側では、senderを確認して発生元を取得できます。
private static void OnStatusChanged(object? sender, EventArgs e){if (sender is Worker worker){Console.WriteLine($"発生元: {worker.Name}");}}同じイベントハンドラーを複数のオブジェクトへ登録する場合、senderを使うと発生元を識別できます。
worker1.Completed += OnCompleted;worker2.Completed += OnCompleted;private static void OnCompleted(object? sender, EventArgs e){if (sender is Worker worker){Console.WriteLine($"{worker.Name}の処理が完了しました。");}}特別な理由がない限り、senderにはnullではなくイベント発生元のthisを渡しましょう。
5-5. データをイベント購読者へ渡す実装例
商品の価格が変更されたことを通知する例を考えます。
public class PriceChangedEventArgs : EventArgs{public PriceChangedEventArgs(decimal oldPrice, decimal newPrice){OldPrice = oldPrice;NewPrice = newPrice;}public decimal OldPrice { get; }public decimal NewPrice { get; }
}
public class Product{private decimal _price;
public Product(string name, decimal price){Name = name;_price = price;}public string Name { get; }public decimal Price{get => _price;set{if (_price == value){return;}decimal oldPrice = _price;_price = value;OnPriceChanged(oldPrice, value);}}public event EventHandler<PriceChangedEventArgs>? PriceChanged;protected virtual void OnPriceChanged(decimal oldPrice,decimal newPrice){var eventArgs = new PriceChangedEventArgs(oldPrice,newPrice);PriceChanged?.Invoke(this, eventArgs);}
}
購読側は次のように記述します。
var product = new Product("キーボード", 5000m);product.PriceChanged += (sender, e) =>{if (sender is Product changedProduct){Console.WriteLine($"{changedProduct.Name}の価格が" +$"{e.OldPrice}円から{e.NewPrice}円へ変更されました。");}};
product.Price = 4500m;
独自のEventArgsを利用すると、イベントに関連する複数の情報を分かりやすく渡せます。
6. eventをクラス外部からInvokeできない理由
6-1. eventは宣言したクラス内からのみ発火できる
eventとして宣言されたメンバーは、原則として宣言したクラスの内部からのみ発火できます。
public class Worker{public event EventHandler? Completed;public void Execute(){Completed?.Invoke(this, EventArgs.Empty);}
}
外部クラスから次のようにInvokeすることはできません。
var worker = new Worker();worker.Completed?.Invoke(worker, EventArgs.Empty);この制限により、イベントの発生条件を宣言元のクラスが管理できます。
外部から自由にイベントを発火できると、実際には処理が終わっていないのにCompletedを発火するなど、オブジェクトの状態と通知内容が一致しなくなる可能性があります。
6-2. 外部クラスから許可されている操作
外部クラスからイベントに対して行える基本操作は、イベントハンドラーの登録と解除です。
worker.Completed += OnCompleted;worker.Completed -= OnCompleted;一方、次のような操作はできません。
worker.Completed = OnCompleted;worker.Completed?.Invoke(worker, EventArgs.Empty);eventによって操作を制限することで、次の問題を防げます。
既存の購読者を外部から上書きされる
関係のないクラスからイベントを発火される
イベントの発火条件が不明確になる
オブジェクトの状態と通知が矛盾する
6-3. protectedなOn〇〇メソッドを用意する設計
イベントの発火処理は、protected virtualのOn〇〇メソッドにまとめるのが一般的です。
public class Worker{public event EventHandler? Completed;protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}public void Execute(){// メイン処理OnCompleted();}
}
この設計には次のメリットがあります。
発火処理を1か所に集約できる
派生クラスで発火時の処理を拡張できる
Nullチェックの書き忘れを防げる
イベント引数の作成方法を統一できる
発火前後の共通処理を追加しやすい
独自のイベント引数を使用する場合は、引数として受け取る形が一般的です。
protected virtual void OnProgressChanged(ProgressChangedEventArgs e){ProgressChanged?.Invoke(this, e);}6-4. 継承先クラスからイベントを発火する方法
基底クラスで宣言されたイベントを、派生クラスから直接Invokeすることはできません。
public class BaseWorker{public event EventHandler? Completed;}public class CustomWorker : BaseWorker{public void Execute(){// Completed?.Invoke(this, EventArgs.Empty);// コンパイルエラー}}
イベントを派生クラスから発火できるようにするには、基底クラスにprotectedなメソッドを用意します。
public class BaseWorker{public event EventHandler? Completed;protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}
}
public class CustomWorker : BaseWorker{public void Execute(){Console.WriteLine("独自処理を実行します。");OnCompleted();}}
派生クラスがOnCompletedをオーバーライドする場合は、通常、基底クラスのメソッドも呼び出します。
protected override void OnCompleted(){Console.WriteLine("イベント発火前の追加処理");base.OnCompleted();}base.OnCompleted()を呼ばない場合、基底クラスのイベントが発火しなくなるため注意が必要です。
6-5. publicメソッド経由でイベントを発火させる方法
外部からの操作をきっかけにイベントを発火させたい場合は、公開メソッドを用意します。
public class Alarm{public event EventHandler? Triggered;public void Trigger(){OnTriggered();}protected virtual void OnTriggered(){Triggered?.Invoke(this, EventArgs.Empty);}
}
外部クラスは、イベントを直接Invokeするのではなく、公開メソッドを呼び出します。
var alarm = new Alarm();alarm.Triggered += (sender, e) =>{Console.WriteLine("アラームが発生しました。");};
alarm.Trigger();
公開メソッド内では、入力チェックや状態確認も実施できます。
public void Trigger(){if (!IsEnabled){return;}OnTriggered();
}
これにより、イベントの発火条件をクラス内で管理できます。
7. eventのInvokeでよくあるエラーと解決方法
7-1. 「イベントは+=または-=の左辺にのみ指定できます」の原因
外部クラスからイベントを参照したり、Invokeしたりすると、次のようなコンパイルエラーが発生します。
イベントは += または -= の左辺にのみ指定できますたとえば、次のコードはエラーになります。
worker.Completed?.Invoke(worker, EventArgs.Empty);CompletedはWorkerクラスで宣言されたイベントであり、外部から許可される操作は購読と購読解除だけだからです。
解決するには、イベントを宣言したクラス内に発火処理を用意します。
public void Complete(){Completed?.Invoke(this, EventArgs.Empty);}または、OnCompletedメソッドに発火処理を集約します。
protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}7-2. Invokeしようとするとコンパイルエラーになる原因
Invoke時のコンパイルエラーには、いくつかの原因があります。
イベント名が間違っているケースです。
Complete?.Invoke(this, EventArgs.Empty);宣言した名前がCompletedなら、正しい名前へ修正します。
Completed?.Invoke(this, EventArgs.Empty);引数の数や型が一致していないケースもあります。
public event EventHandler? Completed;// 引数不足Completed?.Invoke();
EventHandlerには、senderとEventArgsが必要です。
Completed?.Invoke(this, EventArgs.Empty);また、イベントを宣言したクラスの外部や派生クラスから直接発火しようとしていないかも確認しましょう。
7-3. イベントハンドラーの引数が一致しない場合
EventHandlerへ登録するメソッドは、次の形式に合わせる必要があります。
void メソッド名(object? sender, EventArgs e)次のメソッドは引数がないため登録できません。
private static void OnCompleted(){}正しくは次のようにします。
private static void OnCompleted(object? sender, EventArgs e){}EventHandler<TEventArgs>の場合、第2引数は指定したイベント引数型に合わせます。
public event EventHandler<PriceChangedEventArgs>? PriceChanged;private static void OnPriceChanged(object? sender,PriceChangedEventArgs e){}戻り値はvoidである必要があります。
7-4. イベントが発火しても処理が実行されない場合
イベントハンドラーが実行されない場合は、まず購読処理を確認します。
worker.Completed += OnCompleted;発火より前に購読する必要があります。
worker.Execute();worker.Completed += OnCompleted;上記では、Executeの実行後に登録しているため通知を受け取れません。
正しくは次の順序です。
worker.Completed += OnCompleted;worker.Execute();ほかにも、次の点を確認しましょう。
発火条件を満たしているか
途中で
returnしていないかイベントを購読したインスタンスと発火したインスタンスが同じか
発火前に購読解除していないか
例外によって
Invokeまで到達していないか
異なるインスタンスを使用している例は次のとおりです。
var worker1 = new Worker();var worker2 = new Worker();worker1.Completed += OnCompleted;worker2.Execute();
この場合、worker1を購読しているため、worker2のイベントは受け取れません。
7-5. 同じイベントハンドラーが複数回実行される場合
同じハンドラーを複数回登録すると、登録回数分だけ実行されます。
worker.Completed += OnCompleted;worker.Completed += OnCompleted;イベント発火時には、OnCompletedが2回呼び出されます。
重複登録を防ぐ方法の1つは、登録前に解除することです。
worker.Completed -= OnCompleted;worker.Completed += OnCompleted;ただし、根本的にはイベント登録を行う場所やタイミングを整理し、登録処理が複数回実行されない設計にすることが重要です。
特に画面を開くたびに購読するアプリケーションでは、画面を閉じるときに解除する処理が必要です。
7-6. 購読解除を忘れた場合の問題と対処法
長期間存在するオブジェクトのイベントを、短期間しか使わないオブジェクトが購読すると、購読者が不要になっても参照され続けることがあります。
publisher.Updated += subscriber.OnUpdated;発行元のpublisherが購読者への参照を内部的に保持するため、購読者がガベージコレクションの対象にならず、メモリリークの原因になる可能性があります。
不要になったら、-=で解除します。
publisher.Updated -= subscriber.OnUpdated;IDisposableを実装し、Disposeで解除する方法もあります。
public sealed class Subscriber : IDisposable{private readonly Publisher _publisher;private bool _disposed;public Subscriber(Publisher publisher){_publisher = publisher;_publisher.Updated += OnUpdated;}private void OnUpdated(object? sender, EventArgs e){Console.WriteLine("更新通知を受け取りました。");}public void Dispose(){if (_disposed){return;}_publisher.Updated -= OnUpdated;_disposed = true;}
}
匿名ラムダを解除する場合、登録時のデリゲートを変数に保持しておく必要があります。
EventHandler handler = (sender, e) =>{Console.WriteLine("完了しました。");};worker.Completed += handler;worker.Completed -= handler;
見た目が同じラムダ式をもう一度書いても、通常は同じデリゲートとして解除できません。
8. 非同期処理や複数スレッドでeventをInvokeする際の注意点
8-1. asyncメソッド内でイベントを発火する方法
asyncメソッド内でも、通常のイベントは同じ方法で発火できます。
public event EventHandler? Completed;public async Task ExecuteAsync(){await Task.Delay(1000);
Completed?.Invoke(this, EventArgs.Empty);
}
イベントハンドラーが同期メソッドであれば、Invokeが完了するまで各ハンドラーが順番に実行されます。
worker.Completed += (sender, e) =>{Console.WriteLine("完了通知を受け取りました。");};await worker.ExecuteAsync();
ただし、ExecuteAsyncを実行したスレッドと、イベントが発火するスレッドが必ずしも同じとは限りません。UIアプリケーションでは、後述するUIスレッドの問題に注意が必要です。
8-2. イベントハンドラーをasync voidにする際の注意点
標準のEventHandlerは戻り値がvoidであるため、非同期イベントハンドラーはasync voidになります。
private async void OnCompleted(object? sender, EventArgs e){await SaveResultAsync();}イベントハンドラーではasync voidが許容される代表的な場面ですが、次の注意点があります。
呼び出し側が処理完了を
awaitできない発行元は非同期処理の完了を待てない
例外を通常の
Taskとして受け取れない複数の非同期ハンドラーの完了をまとめて管理しにくい
イベントハンドラー内では、必要に応じて例外を処理します。
private async void OnCompleted(object? sender, EventArgs e){try{await SaveResultAsync();}catch (Exception ex){Console.WriteLine($"保存に失敗しました: {ex.Message}");}}発行元が購読者の非同期処理完了を待つ必要があるなら、通常のイベントではなく、Func<T, Task>などを使った非同期コールバック設計を検討します。
public Func<Task>? CompletedAsync { get; set; }public async Task ExecuteAsync(){// メイン処理
if (CompletedAsync != null){await CompletedAsync.Invoke();}
}
ただし、これは一般的なeventとは異なる設計です。
8-3. UIスレッド以外からInvokeする場合の問題
Windows Forms、WPF、.NET MAUIなどのUIアプリケーションでは、UIコントロールを基本的にUIスレッドから操作する必要があります。
バックグラウンドスレッドでイベントを発火し、イベントハンドラーから直接UIを更新すると、例外や予期しない動作が発生する可能性があります。
Windows Formsでは、Control.InvokeやBeginInvokeを使ってUIスレッドへ処理を移します。
private void OnProgressChanged(object? sender,ProgressChangedEventArgs e){if (InvokeRequired){BeginInvoke(() =>{progressBar.Value = e.Progress;}); return;}progressBar.Value = e.Progress;
}
ここで使われるControl.Invokeは、デリゲートのInvokeやイベント発火のInvokeとは目的が異なります。
イベントの
Invoke:登録されているハンドラーを呼び出すControl.Invoke:処理をUIスレッド上で実行する
WPFでは、Dispatcherを利用します。
Application.Current.Dispatcher.Invoke(() =>{ProgressText.Text = $"{e.Progress}%";});UIアプリケーションでは、イベントがどのスレッドから発火するかを明確にしておくことが重要です。
8-4. 複数スレッドからイベントを発火する場合の安全性
イベントを複数スレッドから発火する場合、イベントハンドラー側で共有データを変更すると競合状態が発生する可能性があります。
private int _completedCount;private void OnCompleted(object? sender, EventArgs e){_completedCount++;}
複数スレッドから同時に実行される可能性があるなら、Interlockedやlockを使用します。
private int _completedCount;private void OnCompleted(object? sender, EventArgs e){Interlocked.Increment(ref _completedCount);}
発行側で購読や購読解除と発火が同時に行われる可能性がある場合、?.Invokeはイベントの現在値を評価してから呼び出すため、基本的なNullチェックとして使用できます。
Completed?.Invoke(this, EventArgs.Empty);ただし、イベントハンドラーの実行内容までスレッドセーフになるわけではありません。共有状態へのアクセスは、発行側と購読側の双方で適切に同期する必要があります。
8-5. イベント購読者で発生した例外の扱い方
イベントハンドラーが例外を投げると、その例外はInvokeを実行した発行元へ伝わります。
Completed?.Invoke(this, EventArgs.Empty);複数のハンドラーが登録されている場合、途中のハンドラーで例外が発生すると、それ以降のハンドラーが実行されない可能性があります。
worker.Completed += FirstHandler;worker.Completed += ThrowingHandler;worker.Completed += LastHandler;ThrowingHandlerが例外を投げると、LastHandlerまで到達しないことがあります。
すべてのハンドラーを個別に実行し、例外を個別に処理したい場合は、GetInvocationListを利用できます。
protected virtual void OnCompleted(){EventHandler? handlers = Completed;if (handlers == null){return;}foreach (EventHandler handler in handlers.GetInvocationList()){try{handler(this, EventArgs.Empty);}catch (Exception ex){Console.WriteLine($"イベントハンドラーでエラーが発生しました: {ex.Message}");}}
}
ただし、例外を無条件に握りつぶすと問題の発見が遅れる可能性があります。ログに記録する、複数の例外をまとめる、処理を中断するなど、用途に合った方針を決めましょう。
9. eventとInvokeを使った初心者向けサンプル
9-1. ボタンクリックを想定した基本サンプル
独自のボタンクラスでクリックを通知するサンプルです。
public class SimpleButton{public event EventHandler? Click;public void PerformClick(){Console.WriteLine("ボタンが押されました。");OnClick();}protected virtual void OnClick(){Click?.Invoke(this, EventArgs.Empty);}
}
public class Program{public static void Main(){var button = new SimpleButton();
button.Click += OnButtonClick;button.PerformClick();}private static void OnButtonClick(object? sender, EventArgs e){Console.WriteLine("クリックイベントを受け取りました。");}
}
PerformClickが呼ばれると、OnClickを通じてClickイベントが発火します。
9-2. データ変更を通知するサンプル
名前の変更前後を通知するサンプルです。
public class NameChangedEventArgs : EventArgs{public NameChangedEventArgs(string oldName, string newName){OldName = oldName;NewName = newName;}public string OldName { get; }public string NewName { get; }
}
public class User{private string _name;
public User(string name){_name = name;}public event EventHandler<NameChangedEventArgs>? NameChanged;public string Name{get => _name;set{ArgumentException.ThrowIfNullOrWhiteSpace(value);if (_name == value){return;}string oldName = _name;_name = value;OnNameChanged(oldName, value);}}protected virtual void OnNameChanged(string oldName,string newName){var eventArgs = new NameChangedEventArgs(oldName,newName);NameChanged?.Invoke(this, eventArgs);}
}
使用例は次のとおりです。
var user = new User("佐藤");user.NameChanged += (sender, e) =>{Console.WriteLine($"名前が「{e.OldName}」から「{e.NewName}」へ変更されました。");};
user.Name = "鈴木";
9-3. 処理完了を通知するサンプル
非同期処理の完了をイベントで通知するサンプルです。
public class DataProcessor{public event EventHandler? ProcessingCompleted;public async Task ProcessAsync(){Console.WriteLine("処理を開始します。");await Task.Delay(1000);Console.WriteLine("処理本体が完了しました。");OnProcessingCompleted();}protected virtual void OnProcessingCompleted(){ProcessingCompleted?.Invoke(this, EventArgs.Empty);}
}
使用例は次のとおりです。
var processor = new DataProcessor();processor.ProcessingCompleted += (sender, e) =>{Console.WriteLine("完了イベントを受け取りました。");};
await processor.ProcessAsync();
この例では、ProcessAsyncのメイン処理が終わった後でイベントを発火しています。
9-4. 複数のイベントハンドラーを登録するサンプル
1つのイベントには複数のイベントハンドラーを登録できます。
public class OrderService{public event EventHandler? OrderCompleted;public void CompleteOrder(){Console.WriteLine("注文処理を完了します。");OrderCompleted?.Invoke(this, EventArgs.Empty);}
}
複数の購読者を登録します。
var service = new OrderService();service.OrderCompleted += (sender, e) =>{Console.WriteLine("購入者へメールを送信します。");};
service.OrderCompleted += (sender, e) =>{Console.WriteLine("売上データを更新します。");};
service.OrderCompleted += (sender, e) =>{Console.WriteLine("在庫データを更新します。");};
service.CompleteOrder();
イベントを使うことで、注文完了後の複数処理を、注文処理本体から分離できます。
ただし、標準的なイベントは同期的に呼び出されます。1つのハンドラーに時間がかかると、後続のハンドラーや発行元の処理も待たされます。
9-5. イベントハンドラーを解除するサンプル
名前付きメソッドを購読し、後から解除する例です。
public class TimerNotifier{public event EventHandler? Tick;public void Notify(){Tick?.Invoke(this, EventArgs.Empty);}
}
public class Program{public static void Main(){var notifier = new TimerNotifier();
notifier.Tick += OnTick;notifier.Notify();notifier.Tick -= OnTick;notifier.Notify();}private static void OnTick(object? sender, EventArgs e){Console.WriteLine("通知を受け取りました。");}
}
1回目のNotifyではハンドラーが実行されますが、解除後の2回目では実行されません。
ラムダ式を解除したい場合は、同じデリゲートインスタンスを保持します。
EventHandler handler = (sender, e) =>{Console.WriteLine("通知を受け取りました。");};notifier.Tick += handler;notifier.Tick -= handler;
10. eventを正しく設計・運用するためのポイント
10-1. イベント名の付け方
イベント名は、何が起きたのか分かる名前にします。
public event EventHandler? Completed;public event EventHandler? ValueChanged;public event EventHandler? DataLoaded;public event EventHandler? ErrorOccurred;.NETでは、発生後の通知に過去形を使用する名前が多く見られます。
ChangedCompletedClosedLoaded処理前にキャンセル可能なイベントを用意する場合は、ChangingやClosingなどの進行形を使うことがあります。
public event EventHandler<CancelEventArgs>? Closing;public event EventHandler? Closed;イベント名だけで、発火するタイミングと意味が伝わるようにしましょう。
10-2. On〇〇メソッドに発火処理を集約する
イベントを複数の場所から直接Invokeすると、イベント引数や発火前後の処理がばらばらになりやすくなります。
Completed?.Invoke(this, EventArgs.Empty);発火処理は、次のようなOn〇〇メソッドに集約するのがおすすめです。
protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}処理本体からはOnCompletedを呼び出します。
public void Execute(){PerformMainProcess();OnCompleted();}これにより、ログ出力や共通処理を追加するときも変更箇所を限定できます。
protected virtual void OnCompleted(){Console.WriteLine("Completedイベントを発火します。");Completed?.Invoke(this, EventArgs.Empty);}10-3. イベントの発火条件を明確にする
イベントを設計するときは、どの条件で何回発火するのかを明確にします。
たとえば、ValueChangedイベントなら、次の点を決める必要があります。
同じ値が設定された場合も発火するか
値の変更前と変更後のどちらで発火するか
検証に失敗した場合も発火するか
1回の操作で複数回発火する可能性があるか
一般的には、実際に値が変わった場合だけ発火します。
if (_value == value){return;}int oldValue = _value;_value = value;
OnValueChanged(oldValue, value);
発火条件が曖昧だと、購読者側で二重処理や状態不整合が発生しやすくなります。
10-4. イベントハンドラー内に重い処理を書きすぎない
通常のイベントハンドラーは、Invokeを実行したスレッド上で同期的に呼び出されます。
Completed?.Invoke(this, EventArgs.Empty);イベントハンドラー内に長時間かかる処理があると、イベント発行元の処理も停止します。
private void OnCompleted(object? sender, EventArgs e){Thread.Sleep(5000);}時間のかかる処理は、必要に応じて非同期化を検討します。
private async void OnCompleted(object? sender, EventArgs e){try{await SaveLargeFileAsync();}catch (Exception ex){Console.WriteLine(ex.Message);}}ただし、async voidでは発行元が完了を待てません。処理完了を保証する必要がある場合は、イベント以外の非同期APIを設計する必要があります。
また、イベントハンドラー内で発行元の状態を変更すると、同じイベントが再度発火して無限ループになることもあります。
private void OnValueChanged(object? sender, EventArgs e){if (sender is Counter counter){counter.Value++;}}イベント内から同じイベントの発火条件を再び満たさないよう注意しましょう。
10-5. 不要になったイベント購読を解除する
イベント購読が不要になったら、-=で解除します。
publisher.Updated -= OnUpdated;特に次のケースでは、購読解除を意識する必要があります。
発行元がアプリケーション全体で長期間存在する
購読者が画面や一時オブジェクトである
同じ購読処理が複数回実行される可能性がある
購読者が外部リソースを保持している
画面の表示と非表示を繰り返す
IDisposableを利用して、購読と解除を対にする方法も有効です。
public sealed class ViewModel : IDisposable{private readonly DataService _service;private bool _disposed;public ViewModel(DataService service){_service = service;_service.DataChanged += OnDataChanged;}private void OnDataChanged(object? sender, EventArgs e){// データ変更時の処理}public void Dispose(){if (_disposed){return;}_service.DataChanged -= OnDataChanged;_disposed = true;}
}
イベントの登録場所を決めるときは、解除する場所も同時に決めておくと安全です。
まとめ
C#のeventを発火するときは、デリゲートのInvokeメソッドを使用します。
最も基本的で安全な書き方は次のとおりです。
Completed?.Invoke(this, EventArgs.Empty);?.Invokeを使用すると、イベントの購読者がいない場合でもNullReferenceExceptionを発生させずに処理できます。
イベントを扱う際は、次の点が重要です。
イベントは
eventキーワードで宣言する購読者は
+=でイベントハンドラーを登録する購読解除には
-=を使用する発火時は
?.InvokeでNullチェックを行うsenderには通常thisを渡す追加データがなければ
EventArgs.Emptyを渡すデータを渡す場合は
EventHandler<TEventArgs>を使用する発火処理は
On〇〇メソッドへ集約する外部クラスからイベントを直接
InvokeすることはできないUIスレッドや非同期処理では実行スレッドと例外処理に注意する
不要になったイベント購読は解除する
初心者のうちは、まず次の形を基本パターンとして覚えるとよいでしょう。
public class Worker{public event EventHandler? Completed;public void Execute(){// メイン処理OnCompleted();}protected virtual void OnCompleted(){Completed?.Invoke(this, EventArgs.Empty);}
}
この基本形を身につけることで、処理完了通知、値の変更通知、エラー通知、進捗通知など、さまざまな場面でeventとInvokeを活用できるようになります。

