C# eventsとは?初心者向けにイベントの仕組み・使い方・EventHandlerをわかりやすく解説

はじめに

C#でアプリケーションを作っていると、ボタンがクリックされたとき、値が変更されたとき、処理が完了したときなど、「何かが起きたタイミングで別の処理を実行したい」という場面がよくあります。

このような場面で使われるのが、C# eventsです。

C#のeventは、初心者にとって少しわかりにくい機能です。delegate、EventHandler、sender、EventArgs、+=、-=など、関連する用語が多いからです。

しかし、考え方自体は難しくありません。C# eventsは、「あるクラスで何かが起きたことを、別のクラスへ知らせる仕組み」です。

この記事では、C# eventsの基本から、delegateとの関係、EventHandlerの使い方、実践的なサンプルコード、初心者がつまずきやすいポイントまで、順番にわかりやすく解説します。

1. C# eventsとは?初心者向けにイベントの全体像を解説

1-1. C#のeventは「何かが起きたことを通知する仕組み」

C#のeventとは、あるオブジェクトで発生した出来事を、別のオブジェクトに通知するための仕組みです。

たとえば、次のような出来事を考えてみます。

C#
ボタンがクリックされた
テキストボックスの内容が変わった
ファイルの読み込みが完了した
ダウンロードが終わった
ユーザーの状態が変わった

これらはすべて、「何かが起きた」という状態です。

C# eventsを使うと、あるクラスが「処理が完了しました」「値が変更されました」と通知し、それを受け取った別のクラスが必要な処理を実行できます。

つまり、eventは次のような役割を持ちます。

C#
何かが起きた

イベントを発火する

登録されている処理が呼び出される

C# eventsは、GUIアプリ、ゲーム開発、業務システム、ライブラリ設計など、さまざまな場面で使われる重要な機能です。

1-2. イベントが使われる代表例:ボタンクリック・入力変更・処理完了通知

C# eventsの代表的な例は、ボタンクリックイベントです。

Windows FormsやWPFなどで、ボタンをクリックしたときに処理を実行するコードを見たことがあるかもしれません。

C#
button.Click += Button_Click;

このコードは、「buttonのClickイベントが発生したら、Button_Clickメソッドを実行する」という意味です。

ほかにも、次のような場面でイベントはよく使われます。

入力欄の値が変わったときに画面表示を更新する
処理が完了したときにメッセージを表示する
データの変更を別クラスへ通知する
エラーが発生したときにログを出力する
ゲーム内でプレイヤーがダメージを受けたときにUIを更新する

たとえば、処理完了を通知するイベントは次のようなイメージです。

C#
public event EventHandler Completed;

このイベントを使えば、処理を実行するクラスは「完了した」という事実だけを通知し、完了後に何をするかは別のクラスに任せられます。

1-3. eventsを理解するための3つの登場人物:発行側・購読側・イベントハンドラー

C# eventsを理解するには、3つの登場人物を押さえることが大切です。

1つ目は、イベントを発行する側です。これはpublisherと呼ばれます。

publisherは、「何かが起きた」ときにイベントを発火するクラスです。

C#
public class Timer
{
public event EventHandler TimeUp;
}

2つ目は、イベントを受け取る側です。これはsubscriberと呼ばれます。

subscriberは、publisherのイベントに処理を登録します。

C#
timer.TimeUp += OnTimeUp;

3つ目は、イベントハンドラーです。

イベントハンドラーとは、イベントが発生したときに実行されるメソッドのことです。

C#
private void OnTimeUp(object sender, EventArgs e)
{
Console.WriteLine("時間になりました");
}

整理すると、次のようになります。

C#
publisher    : イベントを発生させる側
subscriber : イベントを受け取る側
handler : イベント発生時に実行されるメソッド

C# eventsは、この3つの関係で成り立っています。

1-4. メソッド呼び出しとの違い:直接呼ぶのではなく「通知を受け取る」

通常のメソッド呼び出しでは、あるクラスが別のクラスのメソッドを直接呼び出します。

C#
receiver.DoSomething();

この場合、呼び出す側は「どの相手の、どのメソッドを呼ぶか」を知っている必要があります。

一方、C# eventsでは、イベントを発行する側は、誰がそのイベントを受け取るのかを知る必要がありません。

C#
Completed?.Invoke(this, EventArgs.Empty);

イベントを発行する側は、ただ「完了しました」と通知するだけです。

その通知を受け取るかどうか、受け取ったあとに何をするかは、購読側が決めます。

この仕組みによって、クラス同士の結びつきを弱くできます。これを疎結合と呼びます。

たとえば、処理を行うクラスが画面表示のことまで知っていると、コードの変更が大変になります。しかし、イベントを使えば、処理クラスは「完了した」という通知だけを出し、画面側がそれを受け取って表示を更新できます。

2. C# eventsの基本構文と使い方

2-1. eventキーワードの基本構文

C#でイベントを定義するには、eventキーワードを使います。

基本構文は次のとおりです。

C#
public event デリゲート型 イベント名;

よく使われる形は、EventHandlerを使った書き方です。

C#
public event EventHandler Completed;

これは、「Completedというイベントを定義する」という意味です。

EventHandlerは、C#で標準的に用意されているデリゲート型です。イベントでは非常によく使われます。

イベント名は、何が起きたのかがわかる名前にするのが一般的です。

C#
public event EventHandler Clicked;
public event EventHandler ValueChanged;
public event EventHandler Completed;
public event EventHandler ErrorOccurred;

C# eventsでは、イベント名を見ただけで「どんな出来事を通知するのか」がわかるようにすることが大切です。

2-2. イベントを定義する側:publisherの役割

イベントを定義し、必要なタイミングで発火する側をpublisherと呼びます。

次の例では、Workerクラスがpublisherです。

C#
public class Worker
{
public event EventHandler Completed;

public void DoWork()
{
Console.WriteLine("作業を開始します");

Console.WriteLine("作業中...");

Completed?.Invoke(this, EventArgs.Empty);
}
}

このコードでは、DoWorkメソッドの最後でCompletedイベントを発火しています。

C#
Completed?.Invoke(this, EventArgs.Empty);

これは、「Completedイベントに登録されているメソッドがあれば実行する」という意味です。

?.を使っているため、イベントに何も登録されていない場合は何も起きません。

2-3. イベントを受け取る側:subscriberの役割

イベントを受け取る側をsubscriberと呼びます。

subscriberは、publisherのイベントに対して、実行したいメソッドを登録します。

C#
public class Program
{
public static void Main()
{
Worker worker = new Worker();

worker.Completed += OnCompleted;

worker.DoWork();
}

private static void OnCompleted(object sender, EventArgs e)
{
Console.WriteLine("作業完了の通知を受け取りました");
}
}

この例では、Programクラスがsubscriberです。

C#
worker.Completed += OnCompleted;

この行によって、Completedイベントが発生したときにOnCompletedメソッドが呼び出されるようになります。

実行すると、次のような流れになります。

C#
作業を開始します
作業中...
作業完了の通知を受け取りました

2-4. +=でイベントを購読する

C# eventsでは、イベントに処理を登録することを「購読する」と表現します。

イベントの購読には、+=演算子を使います。

C#
worker.Completed += OnCompleted;

これは、「workerのCompletedイベントが発生したら、OnCompletedメソッドを実行してほしい」という登録です。

イベントには、複数のイベントハンドラーを登録できます。

C#
worker.Completed += OnCompleted;
worker.Completed += WriteLog;
worker.Completed += SendNotification;

この場合、Completedイベントが発生すると、登録されたメソッドが順番に呼び出されます。

C#
private static void OnCompleted(object sender, EventArgs e)
{
Console.WriteLine("完了しました");
}

private static void WriteLog(object sender, EventArgs e)
{
Console.WriteLine("ログを出力しました");
}

private static void SendNotification(object sender, EventArgs e)
{
Console.WriteLine("通知を送信しました");
}

このように、C# eventsを使うと、1つの出来事に対して複数の処理を簡単に登録できます。

2-5. -=でイベントの購読を解除する

イベントに登録した処理は、-=演算子で解除できます。

C#
worker.Completed -= OnCompleted;

これにより、Completedイベントが発生しても、OnCompletedメソッドは呼び出されなくなります。

たとえば、次のように書けます。

C#
Worker worker = new Worker();

worker.Completed += OnCompleted;
worker.Completed -= OnCompleted;

worker.DoWork();

この場合、OnCompletedは登録後に解除されているため、DoWorkを実行してもOnCompletedは呼び出されません。

イベントの購読解除は、特に長く動作するアプリケーションで重要です。不要になったイベントを購読したままにすると、オブジェクトが解放されず、メモリリークの原因になることがあります。

3. C# eventsとdelegateの関係

3-1. eventはdelegateをベースにした仕組み

C# eventsを理解するには、delegateとの関係を知っておく必要があります。

eventは、delegateをベースにした仕組みです。

delegateは「どのような形のメソッドを登録できるか」を表す型です。eventは、そのdelegateを使って、外部から安全にメソッドを登録・解除できるようにしたものです。

たとえば、次のようにdelegateを定義できます。

C#
public delegate void CompletedHandler();

このdelegateを使ってイベントを定義すると、次のようになります。

C#
public event CompletedHandler Completed;

つまり、eventは単独で存在するのではなく、内部的にはdelegateを利用しています。

3-2. delegateとは「メソッドを変数のように扱う型」

delegateとは、メソッドを変数のように扱うための型です。

たとえば、次のようなメソッドがあるとします。

C#
public static void SayHello()
{
Console.WriteLine("Hello");
}

このメソッドをdelegateに代入できます。

C#
public delegate void MyDelegate();

MyDelegate del = SayHello;
del();

実行すると、SayHelloメソッドが呼び出されます。

C#
Hello

delegateを使うと、「後で実行するメソッド」を変数として保持できます。

イベントもこの仕組みを使っています。イベントに登録されたイベントハンドラーは、delegateとして内部に保持され、イベントが発火したときに実行されます。

3-3. eventを付けることで外部から直接実行されるのを防げる

delegateだけをpublicにすると、外部から直接実行できてしまいます。

C#
public class Worker
{
public Action Completed;
}

このようなコードでは、外部から次のように呼び出せます。

C#
worker.Completed();

これは危険です。

本来、Completedを発火してよいのはWorkerクラス自身だけです。外部のクラスが勝手にCompletedを実行できると、イベントの意味が崩れてしまいます。

そこでeventキーワードを使います。

C#
public class Worker
{
public event EventHandler Completed;
}

eventを付けると、外部からできることは基本的に次の2つに制限されます。

C#
worker.Completed += OnCompleted;
worker.Completed -= OnCompleted;

外部から直接実行することはできません。

C#
worker.Completed(this, EventArgs.Empty); // コンパイルエラー

このように、eventはdelegateを安全に扱うための仕組みです。

3-4. delegateだけを使う場合とeventを使う場合の違い

delegateだけを使う場合とeventを使う場合の大きな違いは、外部からの操作範囲です。

delegateをpublicにすると、外部から代入、上書き、実行ができてしまいます。

C#
worker.Completed = SomeMethod;
worker.Completed();
worker.Completed = null;

一方、eventを使うと、外部からは購読と解除だけができます。

C#
worker.Completed += SomeMethod;
worker.Completed -= SomeMethod;

つまり、eventを使うことで、イベントを発火する権限をクラス内部に限定できます。

使い分けの目安は次のとおりです。

単にメソッドを引数として渡したい場合はdelegate、Action、Funcを使う
何かが起きたことを外部に通知したい場合はeventを使う

C# eventsは、通知の仕組みとして使うのが基本です。

4. EventHandlerとは?C# eventsでよく使う標準デリゲート

4-1. EventHandlerの基本形:object sender, EventArgs e

EventHandlerは、C#のイベントでよく使われる標準デリゲートです。

基本形は次のとおりです。

C#
public delegate void EventHandler(object sender, EventArgs e);

つまり、EventHandlerを使うイベントハンドラーは、次の形で書く必要があります。

C#
private void OnSomethingHappened(object sender, EventArgs e)
{
}

引数は2つあります。

C#
object sender
EventArgs e

senderにはイベントを発生させたオブジェクトが入ります。

EventArgsにはイベントに関する追加情報が入ります。

追加情報がない場合は、EventArgs.Emptyを渡すのが一般的です。

C#
Completed?.Invoke(this, EventArgs.Empty);

4-2. senderには何が入るのか

senderには、イベントを発生させたオブジェクトが入ります。

たとえば、WorkerクラスがCompletedイベントを発生させる場合、senderにはWorkerオブジェクト自身を渡します。

C#
Completed?.Invoke(this, EventArgs.Empty);

ここでのthisは、現在のWorkerインスタンスです。

イベントハンドラー側では、senderを使ってイベント発生元を確認できます。

C#
private static void OnCompleted(object sender, EventArgs e)
{
Console.WriteLine(sender.GetType().Name);
}

必要であれば、キャストして発行元の情報を取得することもできます。

C#
private static void OnCompleted(object sender, EventArgs e)
{
Worker worker = (Worker)sender;
}

ただし、senderをキャストするときは、型が正しいか注意が必要です。

安全に書くなら、次のようにasやisを使います。

C#
private static void OnCompleted(object sender, EventArgs e)
{
if (sender is Worker worker)
{
Console.WriteLine("Workerから通知されました");
}
}

4-3. EventArgsには何が入るのか

EventArgsには、イベントに関する追加データを入れます。

追加情報が不要な場合は、EventArgs.Emptyを使います。

C#
Completed?.Invoke(this, EventArgs.Empty);

たとえば、「値が変更された」というイベントで、新しい値を渡したい場合は、独自のEventArgsクラスを作ります。

C#
public class ValueChangedEventArgs : EventArgs
{
public int NewValue { get; }

public ValueChangedEventArgs(int newValue)
{
NewValue = newValue;
}
}

そして、EventHandler<TEventArgs>を使います。

C#
public event EventHandler<ValueChangedEventArgs> ValueChanged;

イベント発火時に、追加データを渡します。

C#
ValueChanged?.Invoke(this, new ValueChangedEventArgs(100));

イベントハンドラー側では、e.NewValueとして値を受け取れます。

C#
private static void OnValueChanged(object sender, ValueChangedEventArgs e)
{
Console.WriteLine($"新しい値: {e.NewValue}");
}

4-4. EventHandlerを使ったシンプルなイベント実装例

EventHandlerを使った基本的なイベント実装は次のようになります。

C#
using System;

public class Worker
{
public event EventHandler Completed;

public void DoWork()
{
Console.WriteLine("作業を実行しています");

OnCompleted();
}

protected virtual void OnCompleted()
{
Completed?.Invoke(this, EventArgs.Empty);
}
}

public class Program
{
public static void Main()
{
Worker worker = new Worker();

worker.Completed += Worker_Completed;

worker.DoWork();
}

private static void Worker_Completed(object sender, EventArgs e)
{
Console.WriteLine("作業完了イベントを受け取りました");
}
}

実行結果は次のとおりです。

C#
作業を実行しています
作業完了イベントを受け取りました

この例では、Workerがイベントの発行側、Programがイベントの購読側です。

4-5. EventHandler<TEventArgs>で独自データを渡す方法

イベントでデータを渡したい場合は、EventHandler<TEventArgs>を使います。

たとえば、進捗率を通知するイベントを作ってみます。

C#
using System;

public class ProgressChangedEventArgs : EventArgs
{
public int Progress { get; }

public ProgressChangedEventArgs(int progress)
{
Progress = progress;
}
}

public class Downloader
{
public event EventHandler<ProgressChangedEventArgs> ProgressChanged;

public void Download()
{
for (int i = 0; i <= 100; i += 50)
{
OnProgressChanged(i);
}
}

protected virtual void OnProgressChanged(int progress)
{
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(progress));
}
}

public class Program
{
public static void Main()
{
Downloader downloader = new Downloader();

downloader.ProgressChanged += Downloader_ProgressChanged;

downloader.Download();
}

private static void Downloader_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Console.WriteLine($"進捗: {e.Progress}%");
}
}

実行結果は次のようになります。

C#
進捗: 0%
進捗: 50%
進捗: 100%

このように、EventHandler<TEventArgs>を使うと、イベント発生時に必要なデータを安全に渡せます。

5. C# eventsの実装手順をサンプルコードで解説

5-1. イベントを定義する

C# eventsを実装する最初の手順は、イベントを定義することです。

ここでは、温度が変わったときに通知するTemperatureSensorクラスを作ります。

C#
public class TemperatureSensor
{
public event EventHandler TemperatureChanged;
}

この時点では、TemperatureChangedというイベントを定義しただけです。

まだイベントは発火しません。

イベントを定義するときは、「何が起きたことを通知するのか」がわかる名前にします。

C#
TemperatureChanged
Completed
Clicked
Started
Stopped

C# eventsでは、Changed、Completed、Clicked、Occurredなどの名前がよく使われます。

5-2. イベントを発火するメソッドを作る

次に、イベントを発火するメソッドを作ります。

C#では、イベントを発火するためのメソッドをOnイベント名という形にすることがよくあります。

C#
protected virtual void OnTemperatureChanged()
{
TemperatureChanged?.Invoke(this, EventArgs.Empty);
}

このメソッドは、TemperatureChangedイベントを発火します。

?.Invokeを使っているため、イベントに誰も登録していない場合でもエラーになりません。

TemperatureSensor全体は次のようになります。

C#
using System;

public class TemperatureSensor
{
public event EventHandler TemperatureChanged;

private int temperature;

public int Temperature
{
get
{
return temperature;
}
set
{
if (temperature != value)
{
temperature = value;
OnTemperatureChanged();
}
}
}

protected virtual void OnTemperatureChanged()
{
TemperatureChanged?.Invoke(this, EventArgs.Empty);
}
}

Temperatureプロパティの値が変わったときだけ、TemperatureChangedイベントを発火しています。

5-3. イベントハンドラーを作る

次に、イベントが発生したときに実行されるイベントハンドラーを作ります。

EventHandlerを使っている場合、イベントハンドラーは次の形にします。

C#
private static void Sensor_TemperatureChanged(object sender, EventArgs e)
{
Console.WriteLine("温度が変更されました");
}

引数は、object senderとEventArgs eです。

C#
object sender
EventArgs e

senderにはイベント発生元が入ります。

この例では、TemperatureSensorのインスタンスが入ります。

C#
private static void Sensor_TemperatureChanged(object sender, EventArgs e)
{
if (sender is TemperatureSensor sensor)
{
Console.WriteLine($"温度が変更されました: {sensor.Temperature}");
}
}

5-4. イベントにハンドラーを登録する

作成したイベントハンドラーをイベントに登録します。

登録には+=を使います。

C#
sensor.TemperatureChanged += Sensor_TemperatureChanged;

これにより、TemperatureChangedイベントが発生したときに、Sensor_TemperatureChangedメソッドが実行されます。

全体のコードは次のとおりです。

C#
using System;

public class TemperatureSensor
{
public event EventHandler TemperatureChanged;

private int temperature;

public int Temperature
{
get
{
return temperature;
}
set
{
if (temperature != value)
{
temperature = value;
OnTemperatureChanged();
}
}
}

protected virtual void OnTemperatureChanged()
{
TemperatureChanged?.Invoke(this, EventArgs.Empty);
}
}

public class Program
{
public static void Main()
{
TemperatureSensor sensor = new TemperatureSensor();

sensor.TemperatureChanged += Sensor_TemperatureChanged;

sensor.Temperature = 25;
sensor.Temperature = 30;
sensor.Temperature = 30;
}

private static void Sensor_TemperatureChanged(object sender, EventArgs e)
{
if (sender is TemperatureSensor sensor)
{
Console.WriteLine($"温度が変更されました: {sensor.Temperature}");
}
}
}

5-5. 実行結果からイベントの流れを確認する

上記のコードを実行すると、次のような結果になります。

C#
温度が変更されました: 25
温度が変更されました: 30

最後に次のコードを実行しています。

C#
sensor.Temperature = 30;

しかし、すでにTemperatureは30なので値は変わっていません。

そのため、イベントは発火しません。

このサンプルの流れを整理すると、次のようになります。

C#
Temperatureに25を代入

値が変わる

OnTemperatureChangedが呼ばれる

TemperatureChangedイベントが発火

Sensor_TemperatureChangedが実行される

C# eventsでは、このように「何かが起きたら通知し、登録された処理が呼び出される」という流れを作ります。

6. C# eventsの実践例:初心者でもわかる具体的な使いどころ

6-1. ボタンクリックイベントの例

C# eventsの代表例は、ボタンクリックイベントです。

Windows Formsでは、ボタンのClickイベントにイベントハンドラーを登録します。

C#
button.Click += Button_Click;

イベントハンドラーは次のように書きます。

C#
private void Button_Click(object sender, EventArgs e)
{
MessageBox.Show("ボタンがクリックされました");
}

このコードでは、ボタンがクリックされたときにButton_Clickメソッドが実行されます。

ボタン自身は、「クリックされたあとに何をするか」を知りません。Clickイベントを発火するだけです。

そのイベントに対して、開発者が必要な処理を登録します。

これがC# eventsの基本的な考え方です。

6-2. 値が変更されたときに通知する例

値が変更されたときに通知したい場合にも、C# eventsは便利です。

たとえば、ユーザー名が変更されたときに通知するクラスを作ります。

C#
using System;

public class User
{
public event EventHandler NameChanged;

private string name;

public string Name
{
get
{
return name;
}
set
{
if (name != value)
{
name = value;
OnNameChanged();
}
}
}

protected virtual void OnNameChanged()
{
NameChanged?.Invoke(this, EventArgs.Empty);
}
}

使う側は次のようにイベントを購読します。

C#
User user = new User();

user.NameChanged += (sender, e) =>
{
Console.WriteLine("名前が変更されました");
};

user.Name = "Taro";

実行結果は次のとおりです。

C#
名前が変更されました

値の変更を通知するイベントでは、Changedという名前がよく使われます。

6-3. 処理完了時に別クラスへ知らせる例

時間のかかる処理が完了したときに、別のクラスへ知らせたい場合にもイベントを使えます。

C#
using System;

public class ReportGenerator
{
public event EventHandler Completed;

public void Generate()
{
Console.WriteLine("レポートを作成中...");

OnCompleted();
}

protected virtual void OnCompleted()
{
Completed?.Invoke(this, EventArgs.Empty);
}
}

public class Program
{
public static void Main()
{
ReportGenerator generator = new ReportGenerator();

generator.Completed += Generator_Completed;

generator.Generate();
}

private static void Generator_Completed(object sender, EventArgs e)
{
Console.WriteLine("レポート作成が完了しました");
}
}

実行結果は次のとおりです。

C#
レポートを作成中...
レポート作成が完了しました

このように、処理するクラスと完了後の処理を分離できます。

6-4. 複数の処理を同じイベントに登録する例

C# eventsでは、1つのイベントに複数のイベントハンドラーを登録できます。

C#
using System;

public class TaskRunner
{
public event EventHandler Completed;

public void Run()
{
Console.WriteLine("タスクを実行します");

Completed?.Invoke(this, EventArgs.Empty);
}
}

public class Program
{
public static void Main()
{
TaskRunner runner = new TaskRunner();

runner.Completed += ShowMessage;
runner.Completed += WriteLog;
runner.Completed += SendMail;

runner.Run();
}

private static void ShowMessage(object sender, EventArgs e)
{
Console.WriteLine("メッセージを表示しました");
}

private static void WriteLog(object sender, EventArgs e)
{
Console.WriteLine("ログを書き込みました");
}

private static void SendMail(object sender, EventArgs e)
{
Console.WriteLine("メールを送信しました");
}
}

実行結果は次のようになります。

C#
タスクを実行します
メッセージを表示しました
ログを書き込みました
メールを送信しました

1つのイベントに複数の処理を登録できるため、処理を柔軟に追加できます。

6-5. ラムダ式でイベントハンドラーを書く例

イベントハンドラーは、ラムダ式でも書けます。

C#
worker.Completed += (sender, e) =>
{
Console.WriteLine("ラムダ式でイベントを受け取りました");
};

短い処理であれば、ラムダ式を使うとコードが簡潔になります。

完全な例は次のとおりです。

C#
using System;

public class Worker
{
public event EventHandler Completed;

public void DoWork()
{
Console.WriteLine("作業中です");
Completed?.Invoke(this, EventArgs.Empty);
}
}

public class Program
{
public static void Main()
{
Worker worker = new Worker();

worker.Completed += (sender, e) =>
{
Console.WriteLine("作業が完了しました");
};

worker.DoWork();
}
}

実行結果は次のとおりです。

C#
作業中です
作業が完了しました

ただし、ラムダ式で登録したイベントハンドラーは、あとで解除しにくい場合があります。

解除が必要な場合は、メソッドとして定義して登録する方が安全です。

7. C# eventsで初心者がつまずきやすいポイント

7-1. イベントが発火しない原因

C# eventsで初心者がよくつまずくのが、「イベントを定義したのに動かない」という問題です。

よくある原因は、イベントを発火していないことです。

C#
public event EventHandler Completed;

public void DoWork()
{
Console.WriteLine("作業中");
}

このコードでは、Completedイベントを定義していますが、発火していません。

イベントを発火するには、Invokeを呼び出す必要があります。

C#
Completed?.Invoke(this, EventArgs.Empty);

修正すると次のようになります。

C#
public void DoWork()
{
Console.WriteLine("作業中");
Completed?.Invoke(this, EventArgs.Empty);
}

また、イベントハンドラーを登録していない場合も、イベント発生時に何も起きません。

C#
worker.Completed += OnCompleted;

この登録を忘れていないか確認しましょう。

7-2. nullチェックを忘れるとエラーになる

イベントに誰も登録していない状態でInvokeすると、NullReferenceExceptionが発生することがあります。

C#
Completed.Invoke(this, EventArgs.Empty);

Completedに何も登録されていない場合、Completedはnullです。そのため、上記のコードはエラーになります。

安全に書くには、?.Invokeを使います。

C#
Completed?.Invoke(this, EventArgs.Empty);

これは、Completedがnullでない場合だけInvokeするという意味です。

古い書き方では、次のようにnullチェックを行うこともあります。

C#
if (Completed != null)
{
Completed(this, EventArgs.Empty);
}

現在は、?.Invokeを使う書き方が簡潔でよく使われます。

7-3. 同じイベントハンドラーを複数回登録してしまう

同じイベントハンドラーを複数回登録すると、その回数分だけ実行されます。

C#
worker.Completed += OnCompleted;
worker.Completed += OnCompleted;
worker.Completed += OnCompleted;

この状態でCompletedイベントが発生すると、OnCompletedは3回呼び出されます。

C#
完了しました
完了しました
完了しました

意図せず同じ処理が何度も実行される場合は、イベント登録が重複していないか確認しましょう。

対策として、登録前に一度解除する方法があります。

C#
worker.Completed -= OnCompleted;
worker.Completed += OnCompleted;

このようにすると、重複登録を防ぎやすくなります。

7-4. 購読解除しないことでメモリリークが起きる場合

イベントを購読したまま解除しないと、場合によってはメモリリークの原因になります。

特に、長く生き続けるオブジェクトのイベントを、短命なオブジェクトが購読する場合は注意が必要です。

たとえば、アプリケーション全体で使われるサービスのイベントを、画面や一時的なオブジェクトが購読したとします。

C#
service.Updated += OnUpdated;

この画面が不要になっても、イベントの購読が残っていると、サービス側が画面への参照を持ち続けることがあります。

その結果、画面オブジェクトがガベージコレクションされず、メモリに残ってしまう可能性があります。

不要になったら、-=で解除します。

C#
service.Updated -= OnUpdated;

特に、GUIアプリケーションや長時間動作するアプリでは、イベントの購読解除を意識しましょう。

7-5. eventは外部から直接呼び出せない

eventは、外部から直接呼び出すことはできません。

C#
worker.Completed(this, EventArgs.Empty);

このようなコードは、クラスの外部からはコンパイルエラーになります。

イベントを発火できるのは、基本的にイベントを定義しているクラスの内部だけです。

C#
public class Worker
{
public event EventHandler Completed;

public void DoWork()
{
Completed?.Invoke(this, EventArgs.Empty);
}
}

これは、イベントの発火タイミングをクラス自身が管理するためです。

外部のクラスは、イベントを発火するのではなく、イベントを購読します。

C#
worker.Completed += OnCompleted;

C# eventsでは、「発火する側」と「受け取る側」を分けて考えることが大切です。

8. C# eventsのベストプラクティス

8-1. イベント名は「〜Changed」「〜Completed」など意味が伝わる名前にする

イベント名は、何が起きたのかがわかる名前にしましょう。

よく使われる名前の例は次のとおりです。

C#
ValueChanged
NameChanged
Completed
Started
Stopped
Clicked
ErrorOccurred

たとえば、値が変わったことを通知するならValueChanged、処理が完了したことを通知するならCompletedが自然です。

悪い例は、意味があいまいな名前です。

C#
public event EventHandler Event1;
public event EventHandler Do;
public event EventHandler Process;

これでは、いつ発生するイベントなのかがわかりません。

C# eventsでは、イベント名だけで目的が伝わるようにすることが重要です。

8-2. イベント発火用メソッドはOnEventName形式にする

イベントを発火するメソッドは、OnEventName形式にするのが一般的です。

たとえば、Completedイベントなら次のようにします。

C#
protected virtual void OnCompleted()
{
Completed?.Invoke(this, EventArgs.Empty);
}

ValueChangedイベントなら次のようにします。

C#
protected virtual void OnValueChanged()
{
ValueChanged?.Invoke(this, EventArgs.Empty);
}

この形式にしておくと、コードの意図がわかりやすくなります。

また、protected virtualにしておくと、継承先のクラスでイベント発火前後の処理を変更できます。

C#
protected virtual void OnCompleted()
{
Completed?.Invoke(this, EventArgs.Empty);
}

初心者のうちは、イベントを発火するときはOnイベント名メソッドを作る、と覚えておくとよいでしょう。

8-3. データを渡す場合はEventArgsを継承したクラスを使う

イベントで追加データを渡したい場合は、EventArgsを継承したクラスを作ります。

たとえば、スコア変更イベントで新しいスコアを渡す場合は、次のようにします。

C#
public class ScoreChangedEventArgs : EventArgs
{
public int NewScore { get; }

public ScoreChangedEventArgs(int newScore)
{
NewScore = newScore;
}
}

イベント定義は次のようになります。

C#
public event EventHandler<ScoreChangedEventArgs> ScoreChanged;

イベント発火時にデータを渡します。

C#
ScoreChanged?.Invoke(this, new ScoreChangedEventArgs(100));

受け取る側は、e.NewScoreでデータを使えます。

C#
private void OnScoreChanged(object sender, ScoreChangedEventArgs e)
{
Console.WriteLine($"新しいスコア: {e.NewScore}");
}

データを渡すイベントでは、EventHandler<TEventArgs>を使うのが標準的です。

8-4. 不要になったイベントは-=で解除する

イベントの購読が不要になったら、-=で解除しましょう。

C#
worker.Completed -= OnCompleted;

特に、次のような場合は購読解除を意識する必要があります。

画面が閉じられるとき
一時的なオブジェクトが破棄されるとき
長く生きるサービスのイベントを購読しているとき
同じイベントを再登録する可能性があるとき

たとえば、画面クラスでイベントを購読した場合、画面を閉じるタイミングで解除します。

C#
public void Dispose()
{
service.Updated -= OnUpdated;
}

イベント購読は便利ですが、参照関係が残ることがあります。不要な購読は解除する習慣を持つと安全です。

8-5. イベントハンドラーには重すぎる処理を書かない

イベントハンドラーには、重すぎる処理を書かないようにしましょう。

イベントは、何かが起きたタイミングで呼び出されます。イベントハンドラーの処理が重いと、発行元の処理や画面操作が止まったように見えることがあります。

たとえば、ボタンクリックイベントの中で時間のかかる処理を直接実行すると、画面が固まる原因になります。

C#
private void Button_Click(object sender, EventArgs e)
{
HeavyProcess();
}

時間のかかる処理は、非同期処理にする、別スレッドで実行する、処理を分割するなどの工夫が必要です。

イベントハンドラーには、できるだけ短く、わかりやすい処理を書くのが基本です。

9. C# eventsと似た仕組みとの違い

9-1. eventsとdelegateの違い

delegateは、メソッドを変数のように扱うための型です。

eventは、そのdelegateを使って、何かが起きたことを通知する仕組みです。

delegateだけの場合は、外部から実行や上書きができてしまうことがあります。

C#
public Action Completed;

eventを使うと、外部からは基本的に+=と-=だけが可能になります。

C#
public event EventHandler Completed;

つまり、delegateはメソッド参照の仕組み、eventは通知の仕組みです。

C# eventsはdelegateをベースにしていますが、目的は「安全なイベント通知」です。

9-2. eventsとAction・Funcの違い

ActionとFuncもdelegateの一種です。

Actionは戻り値のないメソッドを表します。

C#
Action action = () => Console.WriteLine("Hello");
action();

Funcは戻り値のあるメソッドを表します。

C#
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(1, 2));

ActionやFuncは、コールバックや処理の差し替えに便利です。

一方、eventは「何かが起きたことを通知する」ために使います。

たとえば、次のように考えるとわかりやすいです。

処理を渡したいならActionやFunc
出来事を通知したいならevent

C# eventsは、購読と解除の仕組みを持っているため、通知用途に向いています。

9-3. eventsとObserverパターンの関係

Observerパターンは、あるオブジェクトの状態変化を、複数のオブジェクトへ通知する設計パターンです。

C# eventsは、このObserverパターンを言語機能として簡単に使えるようにしたものと考えられます。

たとえば、ある値が変わったときに複数の処理を実行する場合、イベントが便利です。

C#
model.ValueChanged += UpdateView;
model.ValueChanged += WriteLog;
model.ValueChanged += NotifyUser;

このように、1つの状態変化を複数の購読者に通知できます。

Observerパターンを自分で実装すると、購読者リストを管理したり、通知メソッドを作ったりする必要があります。

C# eventsを使えば、これらを簡潔に書けます。

9-4. eventsとコールバックの違い

コールバックは、「あとで呼び出してもらう処理」を渡す仕組みです。

たとえば、Actionを引数で受け取るメソッドはコールバックの一例です。

C#
public void Execute(Action callback)
{
Console.WriteLine("処理中");
callback();
}

使う側は、次のように処理を渡します。

C#
Execute(() => Console.WriteLine("完了しました"));

一方、eventは、複数の購読者に通知できる仕組みです。

C#
Completed += OnCompleted;
Completed += WriteLog;

コールバックは、特定の処理を渡すイメージです。

eventは、発生した出来事に対して、複数の受け取り側が反応するイメージです。

単発の処理を渡すならコールバック、複数の相手に出来事を通知したいならeventが向いています。

9-5. eventsとasync/awaitはどう使い分けるか

async/awaitは、非同期処理をわかりやすく書くための仕組みです。

たとえば、ファイル読み込みやAPI通信など、時間のかかる処理で使います。

C#
await DownloadAsync();

一方、eventは、何かが起きたことを通知する仕組みです。

C#
DownloadCompleted?.Invoke(this, EventArgs.Empty);

使い分けの目安は次のとおりです。

処理の完了を待ちたい場合はasync/await
何かが起きたことを通知したい場合はevent
進捗や状態変化を複数の相手に知らせたい場合はevent

実際には、async/awaitとeventsを組み合わせることもあります。

C#
public async Task DownloadAsync()
{
await Task.Delay(1000);

Completed?.Invoke(this, EventArgs.Empty);
}

このように、非同期処理が完了したあとにイベントを発火することもできます。

10. C# eventsに関するよくある質問

10-1. C#のeventはいつ使うべき?

C#のeventは、「ある出来事を別のクラスへ通知したいとき」に使います。

たとえば、次のような場面です。

値が変更されたことを通知したい
処理が完了したことを通知したい
ボタンがクリックされたことを通知したい
エラーが発生したことを通知したい
複数の処理に同じ出来事を知らせたい

逆に、単に別のメソッドを1回呼べばよいだけなら、通常のメソッド呼び出しで十分です。

イベントを使うべきか迷ったら、「発行側が受け取り側を知らなくてもよい設計にしたいか」を考えると判断しやすくなります。

10-2. EventHandlerを使わず独自delegateを使ってもいい?

EventHandlerを使わず、独自delegateを使っても問題ありません。

たとえば、次のように書けます。

C#
public delegate void MessageHandler(string message);

public event MessageHandler MessageReceived;

ただし、C#の標準的なイベント設計では、EventHandlerまたはEventHandler<TEventArgs>を使うことが多いです。

C#
public event EventHandler<MessageReceivedEventArgs> MessageReceived;

標準的な形にしておくと、ほかの開発者にも意図が伝わりやすくなります。

初心者のうちは、まずEventHandlerとEventHandler<TEventArgs>を使う書き方を覚えるのがおすすめです。

10-3. eventをstaticにしても問題ない?

eventをstaticにすることはできます。

C#
public static event EventHandler GlobalUpdated;

static eventは、クラス全体で共有されるイベントです。

ただし、static eventは注意が必要です。購読解除を忘れると、オブジェクトへの参照が残りやすく、メモリリークの原因になることがあります。

C#
GlobalService.GlobalUpdated += OnGlobalUpdated;

不要になったら、必ず解除しましょう。

C#
GlobalService.GlobalUpdated -= OnGlobalUpdated;

static eventは便利ですが、影響範囲が広くなりやすいため、必要な場合だけ使うのが安全です。

10-4. イベントハンドラーの登録順は実行順に関係する?

通常、イベントハンドラーは登録された順番で呼び出されます。

C#
worker.Completed += Handler1;
worker.Completed += Handler2;
worker.Completed += Handler3;

この場合、一般的にはHandler1、Handler2、Handler3の順で実行されます。

ただし、イベントハンドラーの実行順に強く依存する設計は避けるべきです。

イベントは、「何かが起きたことを通知する仕組み」です。登録順に依存しすぎると、あとから処理を追加・変更したときに予期しない動作につながることがあります。

順番が重要な処理は、1つのメソッド内で明示的に順序を管理する方がわかりやすくなります。

10-5. 初心者はまず何を覚えればよい?

初心者がC# eventsを学ぶときは、まず次の5つを覚えるとよいです。

eventは「何かが起きたことを通知する仕組み」
イベントを定義するにはeventキーワードを使う
イベントを購読するには+=を使う
イベントを解除するには-=を使う
イベントを発火するには?.Invokeを使う

最初は、EventHandlerを使ったシンプルなコードを理解できれば十分です。

C#
public event EventHandler Completed;

Completed?.Invoke(this, EventArgs.Empty);

そして、受け取る側では次のように登録します。

C#
worker.Completed += OnCompleted;

イベントの基本的な流れは、次のとおりです。

C#
イベントを定義する

イベントハンドラーを作る

+=で登録する

?.Invokeで発火する

登録された処理が実行される

この流れを理解すれば、C# eventsの基本はしっかり押さえられます。

まとめ

C# eventsは、「何かが起きたことを通知する」ための仕組みです。

ボタンクリック、値の変更、処理完了、エラー発生など、さまざまな場面で使われます。

C# eventsを理解するうえで重要なのは、発行側、購読側、イベントハンドラーの関係です。

発行側はイベントを定義し、必要なタイミングで発火します。購読側は+=でイベントハンドラーを登録し、イベントが発生したときに処理を実行します。

基本的なイベント定義は次のようになります。

C#
public event EventHandler Completed;

イベントを発火するには、?.Invokeを使います。

C#
Completed?.Invoke(this, EventArgs.Empty);

イベントに処理を登録するには、+=を使います。

C#
worker.Completed += OnCompleted;

イベントの購読を解除するには、-=を使います。

C#
worker.Completed -= OnCompleted;

また、データを渡したい場合は、EventHandler<TEventArgs>と独自のEventArgsクラスを使います。

C#
public event EventHandler<ValueChangedEventArgs> ValueChanged;

C# eventsは、delegateをベースにした仕組みですが、eventキーワードを使うことで、外部から勝手に実行されることを防げます。

初心者のうちは、まず「eventは通知の仕組み」「+=で登録」「-=で解除」「?.Invokeで発火」「EventHandlerを使う」という基本を押さえましょう。

この流れを理解できれば、C# eventsを使って、クラス同士の結びつきを弱くし、変更に強いコードを書けるようになります。