C#のeventとdelegateの違いとは?仕組み・使い方・実装例を初心者向けに徹底解説

はじめに

C#でイベント処理を学び始めると、ほぼ必ず出てくるのがeventdelegateです。

どちらも「ある処理をあとから呼び出す」「別のクラスに通知する」といった場面で使われるため、初心者のうちは違いがわかりにくいかもしれません。

簡単にいうと、delegateメソッドを変数のように扱うための型です。一方、eventdelegateを使った通知処理を安全に公開するための仕組みです。

たとえば、ボタンがクリックされたときに何らかの処理を実行したい場合、C#ではイベントという仕組みがよく使われます。このイベントの裏側では、delegateによって「呼び出したいメソッド」が管理されています。

この記事では、C#のeventdelegateの違いについて、仕組み・使い方・実装例を初心者向けにわかりやすく解説します。

1. C#のeventとdelegateとは?まず全体像をわかりやすく理解しよう

1-1. delegateは「メソッドを変数のように扱う仕組み」

C#のdelegateは、メソッドを代入できる型です。

通常、変数には数値や文字列を代入します。

C#
int number = 10;
string message = "Hello";

これと似た感覚で、delegateにはメソッドを代入できます。

C#
delegate void MyDelegate();

void SayHello()
{
Console.WriteLine("こんにちは");
}

MyDelegate del = SayHello;
del();

このコードでは、SayHelloというメソッドをdelというdelegate変数に代入しています。そしてdel()を呼び出すことで、代入されたSayHelloメソッドが実行されます。

つまりdelegateは、メソッドへの参照を保持して、あとから呼び出すための仕組みです。

1-2. eventは「delegateを安全に公開する仕組み」

eventは、delegateを使った通知処理を外部に公開するための仕組みです。

たとえば、あるクラスで「処理が完了した」「値が変更された」「ボタンがクリックされた」といったタイミングを、別のクラスに知らせたい場面があります。

そのようなときにeventを使います。

C#
public event EventHandler? Completed;

このように宣言すると、外部のクラスはこのイベントに処理を登録できます。

C#
worker.Completed += OnCompleted;

ただし、外部のクラスはイベントを勝手に発火できません。

C#
// 外部からは呼び出せない
worker.Completed?.Invoke(worker, EventArgs.Empty);

この制限があるため、eventを使うと、クラスの内部状態を守りながら安全に通知処理を公開できます。

1-3. eventとdelegateがよく一緒に使われる理由

C#のeventは、内部的にはdelegateをもとにしています。

delegateが「呼び出すメソッドを保持する型」だとすると、eventは「そのdelegateを安全に外部公開するためのメンバー」です。

たとえば、次のような関係です。

C#
public delegate void NotifyHandler(string message);

public event NotifyHandler? Notified;

このコードでは、NotifyHandlerというdelegate型を定義し、それを使ってNotifiedというeventを宣言しています。

つまり、eventは単独で存在するというよりも、delegateと組み合わせて使われることが多いです。

ただし、実務では毎回独自のdelegateを定義するよりも、EventHandlerEventHandler<TEventArgs>を使う書き方が一般的です。

C#
public event EventHandler? Clicked;
public event EventHandler<MessageEventArgs>? MessageReceived;

1-4. 初心者が混乱しやすいポイント

初心者が混乱しやすいのは、eventdelegateの見た目が似ていることです。

たとえば、次の2つはどちらもメソッドを登録できるように見えます。

C#
public Action? OnCompleted;

public event Action? Completed;

しかし、この2つには大きな違いがあります。

Actionをそのまま公開した場合、外部から代入も呼び出しもできてしまいます。

C#
obj.OnCompleted = SomeMethod;
obj.OnCompleted?.Invoke();

一方、eventとして公開した場合、外部からできるのは基本的に+=による登録と-=による解除だけです。

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

外部から直接呼び出したり、勝手に代入して既存の登録を上書きしたりすることはできません。

この違いを理解すると、eventとdelegateの役割がかなり整理しやすくなります。

2. delegateの基本:仕組み・宣言方法・使い方

2-1. delegateの基本構文

delegateは、次のように宣言します。

C#
delegate 戻り値の型 デリゲート名(引数);

たとえば、引数なし・戻り値なしのメソッドを扱うdelegateは次のように定義できます。

C#
delegate void MyDelegate();

文字列を受け取り、戻り値がないメソッドを扱う場合は次のように書きます。

C#
delegate void MessageDelegate(string message);

整数を2つ受け取り、整数を返すメソッドを扱う場合は次のようになります。

C#
delegate int CalculateDelegate(int x, int y);

delegateに代入できるメソッドは、delegateのシグネチャと一致している必要があります。

シグネチャとは、簡単にいうと「戻り値の型」と「引数の型・数」の組み合わせです。

C#
delegate int CalculateDelegate(int x, int y);

int Add(int a, int b)
{
return a + b;
}

CalculateDelegate calc = Add;
Console.WriteLine(calc(3, 5));

この場合、CalculateDelegateintを2つ受け取り、intを返すメソッドを扱うdelegateです。Addメソッドも同じ形なので代入できます。

2-2. delegateにメソッドを代入する方法

delegateにメソッドを代入する基本形は次のとおりです。

C#
MyDelegate del = MethodName;

具体例を見てみましょう。

C#
using System;

delegate void GreetingDelegate();

class Program
{
static void Main()
{
GreetingDelegate greeting = SayHello;
greeting();
}

static void SayHello()
{
Console.WriteLine("こんにちは");
}
}

実行すると、次のように表示されます。

こんにちは

delegateには、メソッド名だけを代入します。SayHello()のように丸括弧を付けると、その場でメソッドを実行する意味になってしまいます。

正しくは次のように書きます。

C#
GreetingDelegate greeting = SayHello;

誤りやすい書き方は次のとおりです。

C#
// これはメソッドを実行しようとしているため、delegateへの代入ではない
GreetingDelegate greeting = SayHello();

delegateに代入するのは、メソッドの実行結果ではなく、メソッドそのものへの参照です。

2-3. delegateを呼び出す方法

delegateを呼び出すには、通常のメソッドのように()を付けます。

C#
greeting();

引数があるdelegateの場合は、引数も渡します。

C#
delegate void MessageDelegate(string message);

static void ShowMessage(string message)
{
Console.WriteLine(message);
}

MessageDelegate del = ShowMessage;
del("delegateから呼び出しました");

戻り値があるdelegateの場合は、戻り値を受け取ることもできます。

C#
delegate int CalculateDelegate(int x, int y);

static int Add(int x, int y)
{
return x + y;
}

CalculateDelegate calc = Add;
int result = calc(10, 20);

Console.WriteLine(result);

このコードでは、calc(10, 20)を呼び出すことで、実際にはAdd(10, 20)が実行されます。

delegateは、呼び出し側が「具体的にどのメソッドが実行されるか」を意識しなくても処理を呼び出せる点が特徴です。

2-4. 複数のメソッドを登録できるマルチキャストdelegate

delegateには、複数のメソッドを登録できます。これをマルチキャストdelegateといいます。

C#
using System;

delegate void NotifyDelegate();

class Program
{
static void Main()
{
NotifyDelegate notify = MethodA;
notify += MethodB;
notify += MethodC;

notify();
}

static void MethodA()
{
Console.WriteLine("MethodAが実行されました");
}

static void MethodB()
{
Console.WriteLine("MethodBが実行されました");
}

static void MethodC()
{
Console.WriteLine("MethodCが実行されました");
}
}

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

MethodAが実行されました
MethodBが実行されました
MethodCが実行されました

+=を使うことで、delegateに複数のメソッドを追加できます。

逆に、登録したメソッドを解除する場合は-=を使います。

C#
notify -= MethodB;

マルチキャストdelegateは、イベント処理の基礎にもなっています。1つのイベントに複数の処理を登録できるのは、delegateが複数のメソッドを保持できるためです。

ただし、戻り値があるdelegateで複数のメソッドを登録した場合、戻り値として取得できるのは最後に実行されたメソッドの戻り値です。そのため、イベント処理では戻り値のないdelegateがよく使われます。

2-5. Action・Funcとの違い

C#には、あらかじめ用意されている汎用delegateとしてActionFuncがあります。

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

C#
Action<string> action = message => Console.WriteLine(message);
action("Actionを使っています");

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

C#
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 5);

Console.WriteLine(result);

Func<int, int, int>の場合、最初の2つのintが引数の型で、最後のintが戻り値の型です。

C#
Func<引数1, 引数2, 戻り値>

独自のdelegateを定義する代わりに、ActionFuncを使えばコードを短く書けます。

C#
delegate void MessageDelegate(string message);

// 上と似た意味
Action<string> messageAction;

ただし、すべての場面でActionFuncを使えばよいわけではありません。

特にeventとして外部に公開する場合は、C#の標準的なイベントパターンに合わせてEventHandlerEventHandler<TEventArgs>を使うことが多いです。

3. eventの基本:仕組み・宣言方法・使い方

3-1. eventの基本構文

eventは、delegate型を使って宣言します。

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

C#
アクセス修飾子 event デリゲート型 イベント名;

たとえば、独自のdelegateを使ってeventを宣言すると次のようになります。

C#
public delegate void NotifyHandler(string message);

public event NotifyHandler? Notified;

実務では、標準のEventHandlerを使うことも多いです。

C#
public event EventHandler? Completed;

イベントに追加情報を渡したい場合は、EventHandler<TEventArgs>を使います。

C#
public event EventHandler<MessageEventArgs>? MessageReceived;

eventは、クラスの外部に「このタイミングで通知を受け取れます」と公開するためのメンバーです。

3-2. eventにメソッドを登録・解除する方法

eventにメソッドを登録するには、+=を使います。

C#
publisher.Completed += OnCompleted;

登録するメソッドは、イベントのdelegate型と同じシグネチャである必要があります。

C#
void OnCompleted(object? sender, EventArgs e)
{
Console.WriteLine("処理が完了しました");
}

解除する場合は、-=を使います。

C#
publisher.Completed -= OnCompleted;

eventでは、外部から直接代入することはできません。

C#
// 外部クラスからは基本的に不可
publisher.Completed = OnCompleted;

外部からは+=-=で登録・解除する、という使い方になります。

この仕組みによって、他の登録済みイベントハンドラーを誤って上書きしてしまうリスクを防げます。

3-3. eventを発火する方法

eventを発火できるのは、基本的にeventを宣言したクラスの内部だけです。

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

public void DoWork()
{
Console.WriteLine("処理中...");

OnCompleted();
}

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

この例では、DoWorkメソッドの中で処理が完了したあと、OnCompletedメソッドを呼び出しています。

OnCompletedの中では、次のコードでイベントを発火しています。

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

?.はnull条件演算子です。イベントに何も登録されていない場合でも、null参照エラーを防いでくれます。

イベントを受け取る側は、次のように登録します。

C#
Worker worker = new Worker();
worker.Completed += OnWorkerCompleted;

worker.DoWork();

void OnWorkerCompleted(object? sender, EventArgs e)
{
Console.WriteLine("完了通知を受け取りました");
}

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

処理中...
完了通知を受け取りました

3-4. eventが外部から直接呼び出せない理由

eventが外部から直接呼び出せないのは、カプセル化を守るためです。

仮に外部から自由にイベントを発火できると、クラスの状態とは関係ないタイミングで通知が送られてしまいます。

たとえば、次のようなクラスがあるとします。

C#
public class DownloadService
{
public event EventHandler? DownloadCompleted;

public void Download()
{
Console.WriteLine("ダウンロード開始");

// 実際のダウンロード処理

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

このイベントは、本来ダウンロードが完了したときにだけ発火すべきです。

しかし外部から自由に呼び出せると、ダウンロードしていないのに「完了しました」と通知できてしまいます。

eventを使うと、外部からはイベントへの登録と解除だけを許可し、発火はクラス内部に限定できます。

そのため、eventは「安全な通知の公開」に向いています。

3-5. EventHandlerを使った標準的な書き方

C#のイベントでは、EventHandlerを使った書き方が標準的です。

C#
public event EventHandler? Completed;

イベントを発火するときは、通常次のように書きます。

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

senderにはイベントを発生させたオブジェクトを渡します。通常はthisです。

EventArgsにはイベントに関する追加情報を渡します。追加情報がない場合はEventArgs.Emptyを使います。

追加情報がある場合は、EventArgsを継承したクラスを作り、EventHandler<TEventArgs>を使います。

C#
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)
{
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(i));
}
}
}

このように書くと、イベントを受け取る側は進捗率などの情報を取得できます。

4. C#のeventとdelegateの違い

4-1. delegateは型、eventはメンバー

C#のeventとdelegateの最も基本的な違いは、delegateは型であり、eventはメンバーであるという点です。

delegateは、メソッドの形を定義する型です。

C#
public delegate void NotifyHandler(string message);

一方、eventは、そのdelegate型を使ってクラス内に宣言されるメンバーです。

C#
public event NotifyHandler? Notified;

つまり、delegateは「どのようなメソッドを登録できるか」を表し、eventは「通知を受け取るための窓口」を表します。

たとえるなら、delegateは「電話番号の形式」、eventは「実際に外部から登録できる連絡先」のようなものです。

4-2. 呼び出し権限の違い

delegateをpublicフィールドとして公開すると、外部から呼び出せます。

C#
public Action? OnCompleted;

外部クラスから次のように呼び出せてしまいます。

C#
obj.OnCompleted?.Invoke();

一方、eventとして公開した場合、外部から直接呼び出すことはできません。

C#
public event Action? Completed;

外部から次のように呼び出そうとするとコンパイルエラーになります。

C#
// コンパイルエラー
obj.Completed?.Invoke();

eventを発火できるのは、eventを宣言したクラスの内部だけです。

この違いは非常に重要です。

delegateをそのまま公開すると外部から自由に呼び出せますが、eventを使えば「通知を発生させる責任」をクラス内部に限定できます。

4-3. 代入できる範囲の違い

delegateフィールドをpublicにすると、外部から代入できます。

C#
obj.OnCompleted = SomeMethod;

この場合、すでに登録されていた他のメソッドが上書きされる可能性があります。

C#
obj.OnCompleted += MethodA;
obj.OnCompleted += MethodB;

// ここで全部上書きされる
obj.OnCompleted = MethodC;

これに対して、eventでは外部から直接代入できません。

C#
// コンパイルエラー
obj.Completed = SomeMethod;

外部からできるのは、基本的に+=-=だけです。

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

この制限によって、他のイベントハンドラーを誤って消してしまうリスクを減らせます。

4-4. カプセル化の違い

delegateをpublicフィールドとして公開すると、外部から内部のdelegateを自由に操作できてしまいます。

C#
public Action? OnChanged;

この場合、外部クラスは次のような操作ができます。

C#
obj.OnChanged = null;
obj.OnChanged = OtherMethod;
obj.OnChanged?.Invoke();

これは、クラスの内部実装を外部にさらしている状態に近いです。

一方、eventを使うと、外部には登録と解除だけを許可できます。

C#
public event Action? Changed;

外部から勝手にnullを代入したり、直接呼び出したりすることはできません。

このように、eventはdelegateをカプセル化するための仕組みです。

クラス設計において、外部に通知機能を公開したい場合は、delegateをそのまま公開するよりeventを使うほうが安全です。

4-5. 使いどころの違い

delegateは、処理そのものを引数として渡したい場合や、処理を差し替えたい場合に向いています。

たとえば、計算方法を外部から渡したい場合です。

C#
int Calculate(int x, int y, Func<int, int, int> operation)
{
return operation(x, y);
}

int result = Calculate(10, 5, (a, b) => a + b);

この場合、operationには加算・減算・乗算など、任意の処理を渡せます。

一方、eventは、あるクラスの中で何かが起きたことを外部に通知したい場合に向いています。

C#
public event EventHandler? Completed;

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

C#
button.Click += OnButtonClick;
downloadService.Completed += OnDownloadCompleted;
viewModel.PropertyChanged += OnPropertyChanged;

つまり、delegateは「処理を渡す・差し替える」ために使い、eventは「何かが起きたことを知らせる」ために使う、と考えるとわかりやすいです。

4-6. eventとdelegateの違いを表で比較

比較項目delegateevent
正体メソッドを参照する型delegateを安全に公開するメンバー
主な役割メソッドを変数のように扱う外部に通知を公開する
外部からの呼び出しpublicなら可能不可
外部からの代入publicなら可能不可
外部からの登録+=で可能+=で可能
外部からの解除-=で可能-=で可能
カプセル化弱くなりやすい強い
主な用途コールバック、処理の差し替えイベント通知、状態変更通知
代表例Action, Func, 独自delegateClick, Completed, PropertyChanged

この表の中で特に重要なのは、eventは外部から直接呼び出せないという点です。

この制限があるため、eventはクラスの状態や責任を守りながら通知処理を実装できます。

5. eventとdelegateの実装例で理解する

5-1. delegateだけを使ったシンプルな実装例

まずは、delegateだけを使ったシンプルな例を見てみましょう。

C#
using System;

delegate void NotifyDelegate(string message);

class Program
{
static void Main()
{
NotifyDelegate notify = ShowMessage;

notify("処理が完了しました");
}

static void ShowMessage(string message)
{
Console.WriteLine(message);
}
}

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

処理が完了しました

この例では、NotifyDelegateShowMessageメソッドを代入し、notify("処理が完了しました")で呼び出しています。

delegateを使うことで、呼び出すメソッドを変数のように扱えます。

次に、複数のメソッドを登録してみます。

C#
using System;

delegate void NotifyDelegate(string message);

class Program
{
static void Main()
{
NotifyDelegate notify = ShowMessage;
notify += WriteLog;

notify("処理が完了しました");
}

static void ShowMessage(string message)
{
Console.WriteLine($"画面表示: {message}");
}

static void WriteLog(string message)
{
Console.WriteLine($"ログ出力: {message}");
}
}

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

画面表示: 処理が完了しました
ログ出力: 処理が完了しました

このように、delegateには複数のメソッドを登録できます。

5-2. eventを使った通知処理の実装例

次に、eventを使った通知処理を見てみましょう。

C#
using System;

public class Worker
{
public event EventHandler? Completed;

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

// 何らかの処理
Console.WriteLine("作業中...");

OnCompleted();
}

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

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

worker.Completed += Worker_Completed;

worker.DoWork();
}

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

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

作業を開始します
作業中...
作業完了イベントを受け取りました

Workerクラスは、作業が完了したタイミングでCompletedイベントを発火しています。

外部のProgramクラスは、worker.Completed += Worker_Completed;でイベントハンドラーを登録しています。

これにより、Workerクラスは「誰が通知を受け取るか」を知らなくても、作業完了を外部に知らせることができます。

5-3. ボタンがクリックされたときのようなイベント処理例

UIアプリケーションでは、ボタンのクリックイベントが代表的なeventの例です。

ここでは、実際のWindows FormsやWPFではなく、仕組みを理解するために簡単なボタンクラスを自作してみます。

C#
using System;

public class Button
{
public event EventHandler? Clicked;

public void Click()
{
Console.WriteLine("ボタンがクリックされました");

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

class Program
{
static void Main()
{
Button button = new Button();

button.Clicked += OnButtonClicked;

button.Click();
}

static void OnButtonClicked(object? sender, EventArgs e)
{
Console.WriteLine("クリックイベントの処理を実行しました");
}
}

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

ボタンがクリックされました
クリックイベントの処理を実行しました

この例では、ButtonクラスのClickメソッドが呼ばれると、内部でClickedイベントが発火します。

イベントを受け取る側は、Clickedに処理を登録しておくだけです。

この仕組みが、C#のUIアプリケーションにおけるクリックイベントの基本的な考え方です。

5-4. カスタムEventArgsを使った実装例

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

たとえば、メッセージを受け渡すイベントを作る場合は次のように書けます。

C#
using System;

public class MessageEventArgs : EventArgs
{
public string Message { get; }

public MessageEventArgs(string message)
{
Message = message;
}
}

public class MessageService
{
public event EventHandler<MessageEventArgs>? MessageReceived;

public void Receive(string message)
{
Console.WriteLine("メッセージを受信しました");

MessageReceived?.Invoke(this, new MessageEventArgs(message));
}
}

class Program
{
static void Main()
{
MessageService service = new MessageService();

service.MessageReceived += OnMessageReceived;

service.Receive("こんにちは、C#のeventです");
}

static void OnMessageReceived(object? sender, MessageEventArgs e)
{
Console.WriteLine($"受け取ったメッセージ: {e.Message}");
}
}

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

メッセージを受信しました
受け取ったメッセージ: こんにちは、C#のeventです

MessageEventArgsを使うことで、イベント発火時にメッセージ情報を渡せます。

イベントで複数の情報を渡したい場合も、カスタムEventArgsにプロパティを追加すれば対応できます。

C#
public class DownloadProgressEventArgs : EventArgs
{
public string FileName { get; }
public int Progress { get; }

public DownloadProgressEventArgs(string fileName, int progress)
{
FileName = fileName;
Progress = progress;
}
}

このように、イベントに関する情報を1つのクラスにまとめられるのがEventArgsのメリットです。

5-5. eventの登録と解除を含む実践コード

eventを使うときは、登録だけでなく解除も重要です。

次の例では、イベントを登録したあと、途中で解除しています。

C#
using System;

public class TimerLike
{
public event EventHandler? Tick;

public void Run()
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"{i}回目の処理");
Tick?.Invoke(this, EventArgs.Empty);
}
}
}

class Program
{
static void Main()
{
TimerLike timer = new TimerLike();

timer.Tick += OnTick;

Console.WriteLine("イベント登録後:");
timer.Run();

timer.Tick -= OnTick;

Console.WriteLine("イベント解除後:");
timer.Run();
}

static void OnTick(object? sender, EventArgs e)
{
Console.WriteLine("Tickイベントを受け取りました");
}
}

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

イベント登録後:
1回目の処理
Tickイベントを受け取りました
2回目の処理
Tickイベントを受け取りました
3回目の処理
Tickイベントを受け取りました
イベント解除後:
1回目の処理
2回目の処理
3回目の処理

timer.Tick -= OnTick;でイベントハンドラーを解除したあとは、Tickイベントが発火してもOnTickは呼び出されません。

長期間生存するオブジェクトのイベントに登録する場合は、不要になったタイミングで解除することが大切です。

6. eventとdelegateが使われる代表的な場面

6-1. UIアプリケーションのクリックイベント

eventが最もよく使われる場面の1つが、UIアプリケーションのイベント処理です。

たとえば、ボタンがクリックされたとき、テキストボックスの内容が変更されたとき、ウィンドウが閉じられたときなどにeventが使われます。

C#
button.Click += Button_Click;

このコードは、ボタンのClickイベントにButton_Clickメソッドを登録しています。

クリックされたときに、登録されたメソッドが呼び出されます。

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

UIアプリケーションでは、ユーザーの操作に応じて処理を実行する必要があります。そのため、「何かが起きたら通知する」eventの仕組みが非常に相性がよいです。

6-2. クラス間の通知処理

eventは、クラス間の通知処理にもよく使われます。

たとえば、注文処理が完了したら、メール送信クラスやログ出力クラスに通知したい場合があります。

C#
public class OrderService
{
public event EventHandler? OrderCompleted;

public void CompleteOrder()
{
Console.WriteLine("注文処理が完了しました");

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

利用側は、必要な処理をイベントに登録します。

C#
OrderService orderService = new OrderService();

orderService.OrderCompleted += SendMail;
orderService.OrderCompleted += WriteLog;

orderService.CompleteOrder();

このようにすると、OrderServiceはメール送信やログ出力の具体的な処理を知る必要がありません。

「注文が完了した」という事実だけを通知し、何をするかは登録側に任せられます。

これにより、クラス同士の依存関係を弱くできます。

6-3. コールバック処理

delegateは、コールバック処理によく使われます。

コールバックとは、ある処理の途中や完了後に、外部から渡された処理を呼び出す仕組みです。

C#
void Process(Action callback)
{
Console.WriteLine("処理を開始します");

// 何らかの処理

callback();
}

呼び出し側は、実行してほしい処理を渡します。

C#
Process(() =>
{
Console.WriteLine("コールバックが呼び出されました");
});

delegateを使うと、処理の流れを固定しつつ、一部の処理だけを外部から差し替えられます。

たとえば、成功時の処理、失敗時の処理、完了時の処理などを外部から指定する場合に便利です。

6-4. Observerパターン

eventは、Observerパターンの実装にもよく使われます。

Observerパターンとは、あるオブジェクトの状態が変わったときに、それを監視している複数のオブジェクトへ通知する設計パターンです。

eventは、まさにこの「状態が変わったら通知する」という仕組みに向いています。

C#
public class TemperatureSensor
{
public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;

private int _temperature;

public int Temperature
{
get => _temperature;
set
{
if (_temperature == value)
{
return;
}

_temperature = value;
TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(value));
}
}
}

public class TemperatureChangedEventArgs : EventArgs
{
public int Temperature { get; }

public TemperatureChangedEventArgs(int temperature)
{
Temperature = temperature;
}
}

利用側は、温度変更イベントを購読します。

C#
TemperatureSensor sensor = new TemperatureSensor();

sensor.TemperatureChanged += (sender, e) =>
{
Console.WriteLine($"温度が変更されました: {e.Temperature}度");
};

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

このように、eventを使うと、状態変更を複数のオブジェクトに通知しやすくなります。

6-5. 非同期処理や状態変更の通知

eventは、非同期処理や状態変更の通知にも使われます。

たとえば、ダウンロードの進捗が変わったとき、処理が完了したとき、エラーが発生したときなどです。

C#
public class DownloadService
{
public event EventHandler<ProgressEventArgs>? ProgressChanged;
public event EventHandler? Completed;

public void Download()
{
for (int progress = 0; progress <= 100; progress += 50)
{
ProgressChanged?.Invoke(this, new ProgressEventArgs(progress));
}

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

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

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

利用側は、進捗変更と完了通知をそれぞれ受け取れます。

C#
DownloadService service = new DownloadService();

service.ProgressChanged += (sender, e) =>
{
Console.WriteLine($"進捗: {e.Progress}%");
};

service.Completed += (sender, e) =>
{
Console.WriteLine("ダウンロードが完了しました");
};

service.Download();

eventを使うことで、処理を実行するクラスと、通知を受け取って表示やログ出力を行うクラスを分離できます。

7. 初心者がつまずきやすいエラーと注意点

7-1. eventを外部クラスから直接呼び出そうとしてエラーになる

初心者がよく遭遇するのが、eventを外部クラスから直接呼び出そうとしてコンパイルエラーになるケースです。

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

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

// コンパイルエラー
worker.Completed?.Invoke(worker, EventArgs.Empty);
}
}

eventは、宣言したクラスの外部から直接発火できません。

これはC#の仕様であり、eventの重要な役割です。

イベントを発火したい場合は、eventを持つクラスの内部に発火用のメソッドを用意します。

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

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

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

外部クラスは、イベントを呼び出すのではなく、イベントに処理を登録します。

C#
worker.Completed += OnCompleted;
worker.DoWork();

eventは「外部から呼び出すもの」ではなく、「外部から購読するもの」と考えると理解しやすくなります。

7-2. delegateに何も登録されていない状態で呼び出してしまう

delegateやeventに何も登録されていない状態で呼び出すと、null参照エラーになる場合があります。

C#
Action? callback = null;

// NullReferenceExceptionの原因になる
callback();

安全に呼び出すには、nullチェックを行います。

C#
if (callback != null)
{
callback();
}

または、null条件演算子?.を使います。

C#
callback?.Invoke();

eventでも同じです。

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

この書き方であれば、イベントに何も登録されていない場合は何も起きず、登録されている場合だけ呼び出されます。

初心者のうちは、eventやdelegateを呼び出すときは?.Invoke()を使う、と覚えておくと安全です。

7-3. +=と-=の使い忘れ

eventに処理を登録するときは+=を使います。

C#
button.Clicked += OnClicked;

解除するときは-=を使います。

C#
button.Clicked -= OnClicked;

delegateの場合、=で代入することもできます。

C#
Action action = MethodA;

しかし、すでに登録されている処理がある場合に=を使うと、既存の登録を上書きします。

C#
Action action = MethodA;
action += MethodB;

// MethodAとMethodBが登録されている

action = MethodC;

// MethodAとMethodBは消え、MethodCだけになる

eventでは、外部から=で代入できないため、このような上書き事故を防げます。

ただし、クラス内部ではeventを扱う側が適切に+=-=を使う必要があります。

イベントに処理を追加するなら+=、不要になったら-=と覚えておきましょう。

7-4. イベント解除を忘れてメモリリークにつながるケース

eventを使うときは、イベント解除を忘れるとメモリリークにつながることがあります。

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

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

C#
service.Updated += OnUpdated;

この登録を解除しないまま画面を閉じると、サービス側がイベントハンドラーへの参照を持ち続けることがあります。その結果、本来破棄されるはずのオブジェクトがガベージコレクションの対象になりにくくなる場合があります。

不要になったら、次のように解除します。

C#
service.Updated -= OnUpdated;

特に、次のような場面ではイベント解除を意識しましょう。

場面注意点
画面やコンポーネントの破棄時登録したイベントを解除する
長期間動作するサービスへの登録不要になったら解除する
ラムダ式で登録したイベント同じラムダ式を解除しにくい
静的イベントへの登録参照が残りやすいため特に注意

ラムダ式で登録する場合、解除しにくい書き方があります。

C#
service.Updated += (sender, e) =>
{
Console.WriteLine("更新されました");
};

このように匿名のラムダ式を直接登録すると、あとから同じハンドラーを指定して解除するのが難しくなります。

解除が必要な場合は、変数に保持するか、名前付きメソッドを使うとよいです。

C#
EventHandler handler = (sender, e) =>
{
Console.WriteLine("更新されました");
};

service.Updated += handler;
service.Updated -= handler;

7-5. null条件演算子を使った安全なイベント発火

eventを発火するときは、null条件演算子を使った次の書き方がよく使われます。

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

これは、次の処理を短く書いたものです。

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

Completedにイベントハンドラーが登録されていれば呼び出し、登録されていなければ何もしません。

カスタムEventArgsを使う場合も同じです。

C#
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(50));

この書き方は、C#のevent実装で非常によく使われます。

また、イベント発火処理は専用メソッドにまとめると見通しがよくなります。

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

イベント名がCompletedなら、発火用メソッド名はOnCompletedにするのが一般的です。

8. event・delegate・Action・Func・ラムダ式の関係

8-1. delegateとラムダ式の関係

ラムダ式は、delegateに代入できます。

C#
Action action = () =>
{
Console.WriteLine("ラムダ式を実行しました");
};

action();

引数がある場合は、次のように書けます。

C#
Action<string> show = message =>
{
Console.WriteLine(message);
};

show("こんにちは");

戻り値がある場合はFuncを使います。

C#
Func<int, int, int> add = (x, y) => x + y;

Console.WriteLine(add(3, 5));

ラムダ式は、delegateに代入できる匿名メソッドのようなものです。

eventにもラムダ式を登録できます。

C#
button.Clicked += (sender, e) =>
{
Console.WriteLine("クリックされました");
};

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

ただし、あとから解除する必要があるイベントでは、ラムダ式を直接書くと解除しにくい点に注意が必要です。

8-2. Actionを使うべきケース

Actionは、戻り値がない処理をdelegateとして扱いたい場合に便利です。

たとえば、完了後に実行する処理を渡したい場合です。

C#
void Execute(Action onCompleted)
{
Console.WriteLine("処理を実行します");

onCompleted();
}

呼び出し側は、ラムダ式で処理を渡せます。

C#
Execute(() =>
{
Console.WriteLine("完了後の処理です");
});

引数がある場合は、Action<T>を使います。

C#
void ShowMessage(Action<string> output)
{
output("メッセージです");
}

Actionを使うべきケースは、主に次のような場面です。

ケース
戻り値がない処理を渡したい完了時のコールバック
簡単な処理を差し替えたいログ出力先の変更
一時的な処理を引数で受け取りたいテスト用処理の注入

独自delegateを作るほどではない場合、Actionを使うとコードを簡潔にできます。

8-3. Funcを使うべきケース

Funcは、戻り値がある処理をdelegateとして扱いたい場合に使います。

たとえば、計算処理を外部から渡したい場合です。

C#
int Calculate(int x, int y, Func<int, int, int> operation)
{
return operation(x, y);
}

呼び出し側は、加算や減算などの処理を渡せます。

C#
int addResult = Calculate(10, 5, (a, b) => a + b);
int subResult = Calculate(10, 5, (a, b) => a - b);

Console.WriteLine(addResult);
Console.WriteLine(subResult);

Funcの最後の型引数は戻り値です。

C#
Func<int, int, string> func;

この場合、intを2つ受け取り、stringを返す処理を表します。

Funcを使うべきケースは、主に次のような場面です。

ケース
値を返す処理を渡したい計算式を差し替える
条件判定を外部から渡したいフィルタ処理
変換処理を渡したい数値を文字列に変換する

LINQでもFuncはよく使われています。

C#
var evenNumbers = numbers.Where(x => x % 2 == 0);

このx => x % 2 == 0も、内部的にはdelegateとして扱われます。

8-4. eventでActionやFuncを使わないほうがよい場面

eventは、技術的にはActionを使って宣言することもできます。

C#
public event Action? Completed;

これは動作します。

しかし、実務で外部公開するイベントでは、EventHandlerEventHandler<TEventArgs>を使うほうが一般的です。

C#
public event EventHandler? Completed;

追加情報を渡す場合は次のように書きます。

C#
public event EventHandler<CompletedEventArgs>? Completed;

Actionを使ったeventは簡潔ですが、次のような欠点があります。

書き方注意点
event ActionsenderやEventArgsがないため標準パターンから外れる
event Action<string>引数の意味がわかりにくくなりやすい
event Func<T>戻り値の扱いが複雑になりやすい

イベントは「何かが起きたことを通知する」ための仕組みなので、戻り値を期待する設計とは相性がよくありません。

そのため、eventでFuncを使うのは避けたほうがよい場面が多いです。

処理結果を返してほしい場合は、eventではなくdelegateやインターフェース、メソッド呼び出しとして設計するほうが自然です。

8-5. EventHandlerを使うメリット

EventHandlerを使うメリットは、C#の標準的なイベントパターンに沿えることです。

C#
public event EventHandler? Completed;

この書き方では、イベントハンドラーは次の形になります。

C#
void Handler(object? sender, EventArgs e)
{
}

senderにはイベントの発生元が入り、eにはイベント情報が入ります。

追加情報を渡したい場合は、EventHandler<TEventArgs>を使います。

C#
public event EventHandler<MessageEventArgs>? MessageReceived;

ハンドラーは次のようになります。

C#
void OnMessageReceived(object? sender, MessageEventArgs e)
{
Console.WriteLine(e.Message);
}

EventHandlerを使う主なメリットは次のとおりです。

メリット内容
標準的C#の一般的なイベントパターンに沿える
意図が伝わりやすいeventとしての役割が明確になる
拡張しやすいEventArgsで情報を追加できる
統一感が出る他の.NET APIと似た書き方になる

特にライブラリや複数人で開発するコードでは、独自の書き方よりも標準パターンに合わせたほうが読みやすくなります。

9. eventとdelegateの使い分け方

9-1. delegateを使うべきケース

delegateを使うべきなのは、処理そのものを変数のように扱いたい場合です。

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

C#
int Execute(int x, int y, Func<int, int, int> operation)
{
return operation(x, y);
}

呼び出し側で処理を差し替えられます。

C#
int result1 = Execute(10, 5, (a, b) => a + b);
int result2 = Execute(10, 5, (a, b) => a * b);

delegateは、主に次のようなケースに向いています。

ケース具体例
処理を引数として渡したいコールバック
処理を差し替えたい計算方法、判定条件
戻り値を受け取りたいFuncによる変換処理
一時的な処理を渡したいラムダ式による簡易処理

delegateは「通知」よりも「処理を渡す」「処理を差し替える」場面で使うと考えるとわかりやすいです。

9-2. eventを使うべきケース

eventを使うべきなのは、クラスの内部で何かが起きたことを外部に知らせたい場合です。

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

C#
public event EventHandler? Completed;

作業が完了したときにイベントを発火します。

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

eventは、主に次のようなケースに向いています。

ケース具体例
状態変更を通知したい値が変わった
処理完了を通知したいダウンロード完了
ユーザー操作を通知したいボタンクリック
複数の購読者に通知したいログ出力、画面更新
外部に安全な通知口を公開したいpublicなイベント

eventを使うと、通知を受け取る側は自由に処理を登録できますが、イベントを発火する責任はクラス内部に限定できます。

この性質により、安全で見通しのよい設計にしやすくなります。

9-3. 外部に通知したいならeventを使う

外部に「何かが起きた」と知らせたい場合は、基本的にeventを使います。

たとえば、次のような通知です。

C#
public event EventHandler? Saved;
public event EventHandler? Deleted;
public event EventHandler? Connected;
public event EventHandler? Disconnected;

これらはすべて、「クラス内部で何かが起きたこと」を外部に通知するためのイベントです。

eventを使うと、外部クラスは通知を購読できます。

C#
repository.Saved += OnSaved;
connection.Connected += OnConnected;

しかし、外部から勝手に発火することはできません。

この制限が、eventを使う大きな理由です。

外部に通知口を公開したいときにdelegateフィールドをそのままpublicにすると、外部から直接呼び出されたり、代入で上書きされたりする可能性があります。

そのため、通知目的ならeventを使うのが基本です。

9-4. 処理を差し替えたいならdelegateを使う

処理を外部から渡したい、または差し替えたい場合はdelegateを使います。

たとえば、条件判定を外部から渡す例です。

C#
List<int> Filter(List<int> numbers, Func<int, bool> predicate)
{
List<int> result = new List<int>();

foreach (int number in numbers)
{
if (predicate(number))
{
result.Add(number);
}
}

return result;
}

呼び出し側は、条件を自由に指定できます。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };

var evenNumbers = Filter(numbers, x => x % 2 == 0);
var greaterThanThree = Filter(numbers, x => x > 3);

このように、処理の一部を外部から差し替えたい場合はdelegateが向いています。

eventは通知のための仕組みなので、「処理結果を返してほしい」「1つの処理を差し替えたい」という用途ではdelegateやFuncを使うほうが自然です。

9-5. 実務で迷ったときの判断基準

実務でeventとdelegateのどちらを使うべきか迷ったら、次のように考えると判断しやすくなります。

判断ポイント使うもの
何かが起きたことを外部に知らせたいevent
外部から処理を登録して通知を受けたいevent
外部から勝手に呼び出されたくないevent
複数の購読者に通知したいevent
処理そのものを引数として渡したいdelegate
処理を差し替えたいdelegate
戻り値を受け取りたいdelegate / Func
一時的なコールバックを渡したいdelegate / Action

より簡単にまとめると、次のようになります。

通知したいなら event
処理を渡したいなら delegate

外部に公開する通知機能であれば、基本的にはeventを使いましょう。

一方、メソッドの引数として処理を受け取りたい場合や、アルゴリズムの一部を差し替えたい場合はdelegate、またはActionFuncを使うとよいです。

まとめ

C#のeventdelegateは似ているように見えますが、役割は明確に異なります。

delegateは、メソッドを変数のように扱うための型です。メソッドを代入したり、引数として渡したり、あとから呼び出したりできます。

一方、eventは、delegateを使った通知処理を安全に外部公開するための仕組みです。外部からは+=で登録し、-=で解除できますが、直接呼び出したり代入したりすることはできません。

C#のeventとdelegateの違いを理解するうえで、特に重要なのは次のポイントです。

項目内容
delegateメソッドを参照する型
eventdelegateを安全に公開するメンバー
delegateの用途コールバック、処理の差し替え
eventの用途クリック、完了、状態変更などの通知
eventの特徴外部から直接発火できない
実務での標準eventにはEventHandlerを使うことが多い

初心者のうちは、まず「delegateはメソッドを入れる箱」「eventは安全な通知口」と考えると理解しやすいです。

処理を渡したいならdelegate、外部に通知したいならeventを使う。この基本を押さえておけば、C#のイベント処理やコールバック処理をスムーズに理解できるようになります。