C# delegateとは?初心者にもわかる使い方・Action/Funcとの違い・実践サンプルまで徹底解説

はじめに

C#の学習を進めていると、delegateというキーワードに出会うことがあります。

「メソッドを代入する?」
「ActionやFuncと何が違うの?」
「eventやラムダ式とどう関係しているの?」

このように、初心者にとってC#のdelegateは少し理解しづらい概念です。

しかし、delegateを理解すると、C#でよく使われるイベント処理、コールバック、ラムダ式、LINQ、非同期処理などの仕組みがかなり見通しやすくなります。

この記事では、C#のdelegateについて、基本構文から実践的な使い方、ActionFuncとの違い、eventとの関係、よくあるエラーまで初心者向けにわかりやすく解説します。

1. C#のdelegateとは?初心者向けにわかりやすく解説

C#のdelegateとは、簡単に言うと「メソッドを変数のように扱うための型」です。

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

C#
int number = 10;
string name = "Taro";

一方、delegateを使うと、メソッドそのものを変数に代入できます。

C#
MyDelegate method = SayHello;
method();

このように、delegateを使うことで「あとから呼び出す処理」を変数として渡したり、差し替えたりできます。

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

C#では、メソッドは通常その場で呼び出します。

C#
SayHello();

しかし、delegateを使うと、メソッドをいったん変数に入れてから呼び出せます。

C#
delegate void MessageDelegate();

class Program
{
static void Main()
{
MessageDelegate message = SayHello;
message();
}

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

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

こんにちは

ここで重要なのは、MessageDelegateが「戻り値なし、引数なしのメソッドを入れられる型」として定義されている点です。

つまり、delegateはメソッド用の型です。

1-2. delegateが必要になる理由

delegateが必要になる理由は、処理を固定せず、あとから柔軟に変更できるようにするためです。

たとえば、次のような計算処理があるとします。

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

static int Subtract(int a, int b)
{
return a - b;
}

ある場面では足し算を使い、別の場面では引き算を使いたいとします。

このとき、delegateを使えば「どの計算をするか」を外部から渡せます。

C#
delegate int CalculateDelegate(int a, int b);

static void Main()
{
CalculateDelegate calc = Add;
Console.WriteLine(calc(10, 5));

calc = Subtract;
Console.WriteLine(calc(10, 5));
}

実行結果は次の通りです。

15
5

このように、処理を変数として扱えるため、コードの柔軟性が高まります。

1-3. delegateでできることの全体像

C#のdelegateを使うと、主に次のようなことができます。

処理を変数に代入できます。

C#
MessageDelegate message = SayHello;

処理をメソッドの引数として渡せます。

C#
Execute(SayHello);

条件や計算処理を外部から差し替えられます。

C#
Calculate(10, 5, Add);
Calculate(10, 5, Subtract);

複数のメソッドをまとめて呼び出せます。

C#
message += SayGoodMorning;
message += SayGoodNight;

イベント処理の基礎として使われます。

C#
button.Click += Button_Click;

つまり、delegateはC#における「処理を渡す」「処理を差し替える」「処理を通知する」ための重要な仕組みです。

1-4. コールバック・イベント・処理の差し替えとの関係

delegateは、コールバックやイベント処理と深く関係しています。

コールバックとは、「ある処理が終わったあとに呼び出してほしい処理」のことです。

たとえば、データの読み込みが終わったあとにメッセージを表示したい場合、完了後に呼び出すメソッドをdelegateとして渡せます。

C#
delegate void CompleteDelegate();

static void LoadData(CompleteDelegate onComplete)
{
Console.WriteLine("データを読み込み中...");
onComplete();
}

static void ShowCompleteMessage()
{
Console.WriteLine("読み込みが完了しました");
}

このように、delegateを使うと「処理が終わったらこのメソッドを呼んでください」という設計ができます。

また、C#のeventも内部的にはdelegateをもとにした仕組みです。ボタンクリックなどのイベント処理を理解するうえでも、delegateの知識は役立ちます。

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

ここからは、C#のdelegateの基本的な書き方を見ていきましょう。

delegateは次の流れで使います。

まず、delegate型を宣言します。

次に、その型の変数にメソッドを代入します。

最後に、その変数を通してメソッドを呼び出します。

2-1. delegateの宣言方法

delegateの基本構文は次の通りです。

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

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

C#
delegate void MessageDelegate();

これは、「戻り値がvoidで、引数がないメソッドを代入できる型」という意味です。

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

C#
delegate void PrintDelegate(string message);

これは、「戻り値がvoidで、string型の引数を1つ受け取るメソッド」を代入できます。

戻り値がある場合は、次のように書きます。

C#
delegate int CalculateDelegate(int a, int b);

これは、「int型の引数を2つ受け取り、int型を返すメソッド」を代入できます。

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

delegateにメソッドを代入するには、メソッド名を指定します。

C#
delegate void MessageDelegate();

class Program
{
static void Main()
{
MessageDelegate message = SayHello;
}

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

このとき、メソッドの戻り値と引数の型がdelegateの定義と一致している必要があります。

次の例は正しく代入できます。

C#
delegate void MessageDelegate();

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

MessageDelegate message = SayHello;

しかし、次の例はエラーになります。

C#
delegate void MessageDelegate();

static void PrintMessage(string text)
{
Console.WriteLine(text);
}

MessageDelegate message = PrintMessage; // エラー

MessageDelegateは引数なしのメソッドを表す型ですが、PrintMessagestring型の引数を必要とするため、型が一致しません。

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

delegateにメソッドを代入したら、通常のメソッドのように呼び出せます。

C#
delegate void MessageDelegate();

class Program
{
static void Main()
{
MessageDelegate message = SayHello;
message();
}

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

また、Invokeメソッドを使って呼び出すこともできます。

C#
message.Invoke();

通常は、次のように短く書くことが多いです。

C#
message();

ただし、delegate変数がnullの可能性がある場合は、次のようにnull条件演算子を使うと安全です。

C#
message?.Invoke();

2-4. 戻り値あり・引数ありのdelegate

戻り値と引数があるdelegateの例を見てみましょう。

C#
delegate int CalculateDelegate(int a, int b);

class Program
{
static void Main()
{
CalculateDelegate calc = Add;

int result = calc(10, 5);
Console.WriteLine(result);
}

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

実行結果は次の通りです。

15

この場合、CalculateDelegateには次の条件を満たすメソッドを代入できます。

C#
int 型を返す
int 型の引数を2つ受け取る

そのため、次のようなメソッドも代入できます。

C#
static int Multiply(int a, int b)
{
return a * b;
}
C#
calc = Multiply;
Console.WriteLine(calc(10, 5));

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

50

2-5. staticメソッドとインスタンスメソッドの指定方法

delegateには、staticメソッドもインスタンスメソッドも代入できます。

まず、staticメソッドを代入する例です。

C#
delegate void MessageDelegate();

class Program
{
static void Main()
{
MessageDelegate message = SayHello;
message();
}

static void SayHello()
{
Console.WriteLine("staticメソッドです");
}
}

次に、インスタンスメソッドを代入する例です。

C#
delegate void MessageDelegate();

class Greeter
{
public void SayHello()
{
Console.WriteLine("インスタンスメソッドです");
}
}

class Program
{
static void Main()
{
Greeter greeter = new Greeter();

MessageDelegate message = greeter.SayHello;
message();
}
}

インスタンスメソッドを指定する場合は、まずクラスのインスタンスを作成し、そのインスタンス経由でメソッドを指定します。

C#
MessageDelegate message = greeter.SayHello;

3. delegateの実践サンプルコード

ここからは、C#のdelegateを実際にどのように使うのか、サンプルコードを通して見ていきます。

基本的な使い方だけでなく、実務でも使いやすい考え方を意識して解説します。

3-1. 文字列を加工するdelegateのサンプル

まずは、文字列を加工する処理をdelegateで切り替える例です。

C#
delegate string TextFormatter(string text);

class Program
{
static void Main()
{
TextFormatter formatter = ToUpperText;

string result1 = formatter("hello");
Console.WriteLine(result1);

formatter = AddBrackets;

string result2 = formatter("hello");
Console.WriteLine(result2);
}

static string ToUpperText(string text)
{
return text.ToUpper();
}

static string AddBrackets(string text)
{
return "[" + text + "]";
}
}

実行結果は次の通りです。

HELLO
[hello]

この例では、formatterに代入するメソッドを変えることで、文字列の加工方法を切り替えています。

delegateを使わずに書くと、条件分岐が増えやすくなります。

C#
if (type == "upper")
{
// 大文字にする
}
else if (type == "bracket")
{
// かっこを付ける
}

一方、delegateを使うと、「どの処理を使うか」を外から渡せるため、柔軟な設計にしやすくなります。

3-2. 計算処理を切り替えるdelegateのサンプル

次は、足し算、引き算、掛け算などの計算処理を切り替える例です。

C#
delegate int Operation(int a, int b);

class Program
{
static void Main()
{
Execute(10, 5, Add);
Execute(10, 5, Subtract);
Execute(10, 5, Multiply);
}

static void Execute(int a, int b, Operation operation)
{
int result = operation(a, b);
Console.WriteLine(result);
}

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

static int Subtract(int a, int b)
{
return a - b;
}

static int Multiply(int a, int b)
{
return a * b;
}
}

実行結果は次の通りです。

15
5
50

このコードでは、Executeメソッドの中身は変えずに、渡すメソッドだけを変えています。

C#
Execute(10, 5, Add);
Execute(10, 5, Subtract);
Execute(10, 5, Multiply);

このように、delegateを使うと共通処理と個別処理を分離しやすくなります。

3-3. 条件判定にdelegateを使うサンプル

delegateは、条件判定にもよく使われます。

たとえば、数値リストから条件に合う値だけを取り出す処理を作ってみましょう。

C#
delegate bool NumberCondition(int number);

class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };

PrintMatchedNumbers(numbers, IsEven);
PrintMatchedNumbers(numbers, IsGreaterThanThree);
}

static void PrintMatchedNumbers(int[] numbers, NumberCondition condition)
{
foreach (int number in numbers)
{
if (condition(number))
{
Console.WriteLine(number);
}
}
}

static bool IsEven(int number)
{
return number % 2 == 0;
}

static bool IsGreaterThanThree(int number)
{
return number > 3;
}
}

この例では、PrintMatchedNumbersメソッドに条件判定の処理を渡しています。

偶数だけを表示したい場合は、IsEvenを渡します。

C#
PrintMatchedNumbers(numbers, IsEven);

3より大きい数だけを表示したい場合は、IsGreaterThanThreeを渡します。

C#
PrintMatchedNumbers(numbers, IsGreaterThanThree);

このように書くことで、数値を走査する処理は共通化し、判定条件だけを差し替えられます。

3-4. コールバック処理でdelegateを使うサンプル

delegateは、コールバック処理にも使えます。

コールバックとは、ある処理が完了したタイミングで呼び出される処理のことです。

C#
delegate void CompleteCallback(string message);

class Program
{
static void Main()
{
DownloadData(OnDownloadComplete);
}

static void DownloadData(CompleteCallback callback)
{
Console.WriteLine("ダウンロード中...");

callback("ダウンロードが完了しました");
}

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

実行結果は次の通りです。

ダウンロード中...
ダウンロードが完了しました

この例では、DownloadDataメソッドが完了したあとに、callbackを呼び出しています。

C#
callback("ダウンロードが完了しました");

これにより、DownloadData側は「完了後に何をするか」を知らなくてもよくなります。

表示するのか、ログに保存するのか、別の処理を実行するのかは、呼び出し側が決められます。

3-5. 実務で使いやすいdelegateの書き方

実務では、独自のdelegateを毎回定義するよりも、ActionFuncを使うことが多いです。

たとえば、次のような独自delegateがあります。

C#
delegate int Operation(int a, int b);

これは、Func<int, int, int>で置き換えられます。

C#
Func<int, int, int> operation = Add;

戻り値がない場合は、Actionを使えます。

C#
Action<string> print = Console.WriteLine;
print("こんにちは");

ただし、意味のある名前を付けたい場合は、独自delegateも有効です。

C#
delegate bool UserValidator(User user);

このように書くと、Func<User, bool>よりも「ユーザー検証を表す処理」であることが明確になります。

実務では、次のように考えるとよいでしょう。

汎用的な処理ならActionFuncを使います。

C#
Func<int, int, int> calc;
Action<string> logger;

業務上の意味を型名で表したい場合は、独自delegateを使います。

C#
delegate bool OrderFilter(Order order);
delegate void PaymentCompletedHandler(Payment payment);

4. マルチキャストdelegateとは?

C#のdelegateは、1つのメソッドだけでなく、複数のメソッドを登録できます。

このように、複数のメソッドを順番に呼び出せるdelegateをマルチキャストdelegateと呼びます。

4-1. 複数のメソッドをdelegateに登録する方法

複数のメソッドをdelegateに登録するには、+=を使います。

C#
delegate void NotifyDelegate();

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

notify();
}

static void SendEmail()
{
Console.WriteLine("メールを送信しました");
}

static void WriteLog()
{
Console.WriteLine("ログを書き込みました");
}

static void ShowMessage()
{
Console.WriteLine("画面にメッセージを表示しました");
}
}

実行結果は次の通りです。

メールを送信しました
ログを書き込みました
画面にメッセージを表示しました

このように、1つのdelegateを呼び出すだけで、登録された複数のメソッドが順番に実行されます。

4-2. +=と-=によるメソッドの追加・削除

delegateにメソッドを追加するには+=を使います。

C#
notify += WriteLog;

登録済みのメソッドを削除するには-=を使います。

C#
notify -= WriteLog;

サンプルを見てみましょう。

C#
delegate void NotifyDelegate();

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

notify -= WriteLog;

notify();
}

static void SendEmail()
{
Console.WriteLine("メールを送信しました");
}

static void WriteLog()
{
Console.WriteLine("ログを書き込みました");
}

static void ShowMessage()
{
Console.WriteLine("画面にメッセージを表示しました");
}
}

実行結果は次の通りです。

メールを送信しました
画面にメッセージを表示しました

WriteLogを削除したため、ログ出力は実行されません。

4-3. 呼び出し順序と戻り値の注意点

マルチキャストdelegateでは、メソッドは基本的に登録した順番で呼び出されます。

C#
notify += MethodA;
notify += MethodB;
notify += MethodC;

この場合、通常は次の順番で実行されます。

MethodA
MethodB
MethodC

ただし、戻り値があるマルチキャストdelegateには注意が必要です。

C#
delegate int NumberDelegate();

class Program
{
static void Main()
{
NumberDelegate number = GetOne;
number += GetTwo;
number += GetThree;

int result = number();

Console.WriteLine(result);
}

static int GetOne()
{
return 1;
}

static int GetTwo()
{
return 2;
}

static int GetThree()
{
return 3;
}
}

実行結果は次の通りです。

3

複数のメソッドが呼び出されますが、最終的に受け取れる戻り値は最後に実行されたメソッドの戻り値です。

そのため、マルチキャストdelegateは、基本的に戻り値がvoidの処理で使うのがわかりやすいです。

4-4. マルチキャストdelegateの実践例

実務に近い例として、注文完了時に複数の処理を行うケースを考えてみましょう。

注文が完了したら、次のような処理を行いたいとします。

メール送信

ログ出力

在庫更新

画面通知

これをマルチキャストdelegateで書くと、次のようになります。

C#
delegate void OrderCompletedDelegate(string orderId);

class Program
{
static void Main()
{
OrderCompletedDelegate orderCompleted = SendMail;
orderCompleted += WriteLog;
orderCompleted += UpdateStock;
orderCompleted += ShowNotification;

CompleteOrder("ORD-001", orderCompleted);
}

static void CompleteOrder(string orderId, OrderCompletedDelegate onCompleted)
{
Console.WriteLine($"注文 {orderId} を完了しました");

onCompleted?.Invoke(orderId);
}

static void SendMail(string orderId)
{
Console.WriteLine($"{orderId}: メールを送信しました");
}

static void WriteLog(string orderId)
{
Console.WriteLine($"{orderId}: ログを書き込みました");
}

static void UpdateStock(string orderId)
{
Console.WriteLine($"{orderId}: 在庫を更新しました");
}

static void ShowNotification(string orderId)
{
Console.WriteLine($"{orderId}: 通知を表示しました");
}
}

実行結果は次の通りです。

注文 ORD-001 を完了しました
ORD-001: メールを送信しました
ORD-001: ログを書き込みました
ORD-001: 在庫を更新しました
ORD-001: 通知を表示しました

注文完了後に実行する処理をあとから追加・削除しやすい点が、マルチキャストdelegateのメリットです。

5. delegateとAction・Funcの違い

C#では、独自にdelegateを定義しなくても、ActionFuncという便利な組み込みのdelegate型を使えます。

初心者が混乱しやすいポイントですが、ActionFuncもdelegateの一種です。

つまり、次のような関係です。

delegate: メソッドを表す型の仕組み
Action: 戻り値なしのdelegate
Func: 戻り値ありのdelegate

5-1. Actionとは?戻り値なしの処理を表す型

Actionは、戻り値がない処理を表すdelegate型です。

たとえば、引数なしで戻り値なしのメソッドは、Actionで表せます。

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

Action action = SayHello;
action();

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

C#
static void PrintMessage(string message)
{
Console.WriteLine(message);
}

Action<string> print = PrintMessage;
print("こんにちは");

複数の引数がある場合も使えます。

C#
Action<string, int> showUser = ShowUser;

static void ShowUser(string name, int age)
{
Console.WriteLine($"{name}さんは{age}歳です");
}

Actionは戻り値がvoidの処理に使う、と覚えておくとわかりやすいです。

5-2. Funcとは?戻り値ありの処理を表す型

Funcは、戻り値がある処理を表すdelegate型です。

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

Func<int, int, int> calc = Add;

int result = calc(10, 5);
Console.WriteLine(result);

ここで、Func<int, int, int>の意味は次の通りです。

1つ目の int: 第1引数の型
2つ目の int: 第2引数の型
3つ目の int: 戻り値の型

Funcでは、最後に書いた型が戻り値の型になります。

たとえば、文字列を受け取って文字数を返す処理は、次のように書けます。

C#
Func<string, int> getLength = text => text.Length;

int length = getLength("Hello");
Console.WriteLine(length);

この場合、stringが引数の型、intが戻り値の型です。

5-3. delegate・Action・Funcの使い分け

delegateActionFuncは、次のように使い分けるとわかりやすいです。

戻り値がない処理にはActionを使います。

C#
Action<string> logger = message => Console.WriteLine(message);

戻り値がある処理にはFuncを使います。

C#
Func<int, int, int> add = (a, b) => a + b;

処理に明確な意味を持たせたい場合は、独自delegateを使います。

C#
delegate bool CustomerFilter(Customer customer);

ActionFuncは短く書けて便利ですが、型名だけでは処理の意味がわかりにくい場合があります。

たとえば、次のコードは型としては正しいですが、何をする処理なのかは名前を見ないとわかりません。

C#
Func<Customer, bool> filter;

一方、独自delegateを使うと意味が明確になります。

C#
delegate bool CustomerFilter(Customer customer);

大規模な業務コードでは、可読性を高めるために独自delegateを使う選択肢もあります。

5-4. 独自delegateを定義すべきケース

独自delegateを定義すべきなのは、次のようなケースです。

業務上の意味を型名で表したい場合です。

C#
delegate bool ProductValidator(Product product);

イベントハンドラーとして意味のある名前を付けたい場合です。

C#
delegate void OrderCompletedHandler(object sender, Order order);

同じシグネチャの処理が複数あり、用途を区別したい場合です。

C#
delegate bool UserFilter(User user);
delegate bool UserValidator(User user);

どちらもFunc<User, bool>で表せますが、UserFilterUserValidatorでは意味が異なります。

このように、独自delegateは「型名で意図を伝えたいとき」に役立ちます。

5-5. Action/Funcで書き換えたサンプルコード

先ほどの計算処理のdelegateを、Funcで書き換えてみましょう。

独自delegateを使った例です。

C#
delegate int Operation(int a, int b);

class Program
{
static void Main()
{
Operation operation = Add;
Console.WriteLine(operation(10, 5));
}

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

これをFuncで書くと、次のようになります。

C#
class Program
{
static void Main()
{
Func<int, int, int> operation = Add;
Console.WriteLine(operation(10, 5));
}

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

さらにラムダ式を使うと、もっと短く書けます。

C#
Func<int, int, int> operation = (a, b) => a + b;

Console.WriteLine(operation(10, 5));

戻り値がない処理の場合は、Actionを使います。

C#
Action<string> print = message => Console.WriteLine(message);

print("こんにちは");

実務では、簡単な処理はActionFunc、意味を明確にしたい処理は独自delegate、という使い分けがよく使われます。

6. delegateとラムダ式・匿名メソッドの関係

C#のdelegateは、ラムダ式や匿名メソッドと一緒に使うことで、さらに簡潔に書けます。

特に現代のC#では、メソッドを別に定義せず、ラムダ式で処理をその場に書くことが多くあります。

6-1. 匿名メソッドとは?

匿名メソッドとは、名前のないメソッドのことです。

通常、メソッドには名前を付けます。

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

しかし、匿名メソッドを使うと、メソッド名を定義せずにdelegateへ直接処理を書けます。

C#
delegate void MessageDelegate();

class Program
{
static void Main()
{
MessageDelegate message = delegate()
{
Console.WriteLine("こんにちは");
};

message();
}
}

このように、delegateキーワードを使って、その場で処理を定義できます。

匿名メソッドは、短い処理を一度だけ使いたい場合に便利です。

6-2. ラムダ式でdelegateを簡潔に書く方法

ラムダ式を使うと、匿名メソッドよりもさらに短く書けます。

匿名メソッドの例です。

C#
MessageDelegate message = delegate()
{
Console.WriteLine("こんにちは");
};

ラムダ式では、次のように書けます。

C#
MessageDelegate message = () =>
{
Console.WriteLine("こんにちは");
};

1行で書ける処理なら、さらに短くできます。

C#
MessageDelegate message = () => Console.WriteLine("こんにちは");

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

C#
delegate int Operation(int a, int b);

Operation add = (a, b) => a + b;

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

この例では、(a, b) => a + bがラムダ式です。

=>の左側に引数、右側に処理を書きます。

6-3. delegateとラムダ式のサンプル比較

通常のメソッドをdelegateに代入する書き方です。

C#
delegate int Operation(int a, int b);

class Program
{
static void Main()
{
Operation add = Add;
Console.WriteLine(add(10, 5));
}

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

匿名メソッドで書くと、次のようになります。

C#
delegate int Operation(int a, int b);

class Program
{
static void Main()
{
Operation add = delegate(int a, int b)
{
return a + b;
};

Console.WriteLine(add(10, 5));
}
}

ラムダ式で書くと、次のようになります。

C#
delegate int Operation(int a, int b);

class Program
{
static void Main()
{
Operation add = (a, b) => a + b;

Console.WriteLine(add(10, 5));
}
}

同じ処理でも、ラムダ式を使うとかなり簡潔になります。

特に、ActionFuncとラムダ式はよく一緒に使われます。

C#
Func<int, int, int> add = (a, b) => a + b;
Action<string> print = message => Console.WriteLine(message);

6-4. ラムダ式を使うときの注意点

ラムダ式は便利ですが、何でも短く書けばよいわけではありません。

処理が長くなる場合は、通常のメソッドとして分けたほうが読みやすくなります。

読みにくい例です。

C#
Func<int, bool> check = x =>
{
int value = x * 2;
value += 10;
value = value / 3;
return value > 5;
};

このような場合は、名前付きメソッドにしたほうが意図が伝わりやすいです。

C#
static bool IsValidNumber(int x)
{
int value = x * 2;
value += 10;
value = value / 3;
return value > 5;
}

また、ラムダ式の中で外側の変数を使う場合にも注意が必要です。

C#
int count = 0;

Action action = () =>
{
count++;
};

action();
Console.WriteLine(count);

このように、ラムダ式は外側の変数を参照できます。この仕組みは便利ですが、意図しないタイミングで値が変わるとバグの原因になることがあります。

7. delegateとイベントの違い

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

イベント処理を理解するには、delegateとの違いを知っておくことが重要です。

7-1. eventはdelegateを安全に扱うための仕組み

delegateは、外部から直接代入したり、呼び出したりできます。

C#
public NotifyDelegate Notify;

このように公開されたdelegateは、外部から次のように上書きされる可能性があります。

C#
publisher.Notify = SomeMethod;

また、外部から直接呼び出すこともできてしまいます。

C#
publisher.Notify();

これは、イベント通知の仕組みとしては危険です。

一方、eventを使うと、外部からできる操作を基本的に+=-=に制限できます。

C#
public event NotifyDelegate Notify;

これにより、外部のコードはイベントに処理を追加・削除できますが、勝手に上書きしたり、直接呼び出したりしにくくなります。

つまり、eventはdelegateをより安全に公開するための仕組みです。

7-2. delegateだけでイベント風の処理を書く例

まずは、delegateだけでイベントのような処理を書いてみます。

C#
delegate void NotifyDelegate(string message);

class Button
{
public NotifyDelegate Clicked;

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

Clicked?.Invoke("クリックイベント発生");
}
}

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

button.Clicked += ShowMessage;
button.Click();

button.Clicked = null;
}

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

このコードは動作しますが、次のように外部からClickednullにできてしまいます。

C#
button.Clicked = null;

また、外部から直接呼び出すことも可能です。

C#
button.Clicked("外部から呼び出し");

これでは、クラスの内部状態やイベントの管理が崩れやすくなります。

7-3. eventを使った実践サンプル

eventを使うと、より安全なイベント処理を書けます。

C#
delegate void NotifyDelegate(string message);

class Button
{
public event NotifyDelegate Clicked;

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

Clicked?.Invoke("クリックイベント発生");
}
}

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

button.Clicked += ShowMessage;
button.Click();

button.Clicked -= ShowMessage;
}

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

この場合、外部からできるのは主に次の操作です。

C#
button.Clicked += ShowMessage;
button.Clicked -= ShowMessage;

一方、外部から次のような操作はできません。

C#
button.Clicked = null;
button.Clicked("外部から呼び出し");

イベントを発生させる責任は、Buttonクラスの内部に限定されます。

このように、eventを使うことで、delegateを安全に公開できます。

7-4. delegateとeventの使い分け

delegateeventは、目的に応じて使い分けます。

メソッドを引数として渡したい場合や、処理を差し替えたい場合はdelegateActionFuncを使います。

C#
void Execute(Action action)
{
action();
}

外部に「何かが起きたこと」を通知したい場合はeventを使います。

C#
public event EventHandler Completed;

実務では、イベントには標準のEventHandlerEventHandler<TEventArgs>を使うことも多いです。

C#
public event EventHandler? Completed;

独自delegateを使ったイベントも書けますが、一般的なイベント設計ではEventHandler系を使うと他の開発者にも伝わりやすくなります。

8. delegateを使うメリット・デメリット

delegateは便利な仕組みですが、使いどころを間違えるとコードが読みにくくなることもあります。

ここでは、C#でdelegateを使うメリットとデメリットを整理します。

8-1. 処理を柔軟に差し替えられる

delegateの大きなメリットは、処理を柔軟に差し替えられることです。

たとえば、同じデータ処理でも、並び替え条件や絞り込み条件だけを外から渡せます。

C#
Func<int, bool> condition = x => x % 2 == 0;

このように、条件や処理を固定せずに渡せるため、再利用しやすいコードになります。

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

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

return result;
}

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

C#
Filter(numbers, x => x % 2 == 0);
Filter(numbers, x => x > 10);
Filter(numbers, x => x < 0);

8-2. 共通処理を再利用しやすくなる

delegateを使うと、共通処理と個別処理を分けやすくなります。

たとえば、処理の前後にログを出力する共通処理を考えます。

C#
static void ExecuteWithLog(Action action)
{
Console.WriteLine("処理開始");

action();

Console.WriteLine("処理終了");
}

呼び出し側は、実行したい処理だけを渡します。

C#
ExecuteWithLog(() => Console.WriteLine("データを保存します"));
ExecuteWithLog(() => Console.WriteLine("メールを送信します"));

実行結果は次の通りです。

処理開始
データを保存します
処理終了
処理開始
メールを送信します
処理終了

ログ出力のような共通処理を1か所にまとめられるため、コードの重複を減らせます。

8-3. コードが複雑になりやすいケース

delegateは便利ですが、使いすぎると処理の流れが追いにくくなります。

たとえば、次のようなコードは、どの処理がいつ実行されるのか見えにくくなります。

C#
service.Execute(
x => repository.Save(x),
y => logger.Write(y),
z => notifier.Send(z)
);

短いコードに見えても、実際には複数の処理が渡されており、初心者には読みづらい場合があります。

また、delegateに代入されたメソッドが別ファイルや別クラスにある場合、実行される処理を追うためにコードを行き来する必要があります。

処理が複雑な場合は、delegateよりもクラスやインターフェースで設計したほうがわかりやすいこともあります。

8-4. 初心者がdelegateでつまずきやすいポイント

初心者がdelegateでつまずきやすいポイントは、主に次のようなものです。

メソッドの型が一致していないことです。

C#
delegate void MyDelegate();

static void Method(string text)
{
}

この場合、引数の数が違うため代入できません。

次に、delegate変数がnullなのに呼び出してしまうことです。

C#
MyDelegate method = null;
method(); // 例外

安全に呼び出すには、次のように書きます。

C#
method?.Invoke();

また、ActionFuncと独自delegateの関係で混乱することもあります。

ActionFuncは、あらかじめ用意されているdelegate型です。

そのため、基本的には次のように覚えると理解しやすくなります。

delegate: メソッドを扱う型の仕組み
Action: 戻り値なしのdelegate
Func: 戻り値ありのdelegate
event: delegateを安全に公開する仕組み

9. delegateのよくあるエラーと対処法

ここでは、C#のdelegateでよくあるエラーと対処法を解説します。

エラーの多くは、型の不一致、null、ラムダ式の書き方、マルチキャストdelegateの戻り値、eventとの混同によって起こります。

9-1. 引数や戻り値の型が一致しない

delegateに代入できるメソッドは、戻り値の型と引数の型が一致している必要があります。

たとえば、次のコードはエラーになります。

C#
delegate int CalculateDelegate(int a, int b);

static void Add(int a, int b)
{
Console.WriteLine(a + b);
}

CalculateDelegate calc = Add; // エラー

CalculateDelegateintを返す必要がありますが、Addメソッドはvoidです。

修正するには、戻り値を一致させます。

C#
delegate int CalculateDelegate(int a, int b);

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

CalculateDelegate calc = Add;

引数の型や数も一致している必要があります。

C#
delegate void PrintDelegate(string message);

static void PrintNumber(int number)
{
Console.WriteLine(number);
}

PrintDelegate print = PrintNumber; // エラー

この場合、stringを受け取るdelegateに、intを受け取るメソッドを代入しようとしているためエラーになります。

9-2. nullのdelegateを呼び出してしまう

delegate変数がnullの状態で呼び出すと、NullReferenceExceptionが発生します。

C#
delegate void MessageDelegate();

MessageDelegate message = null;

message(); // NullReferenceException

対処法として、呼び出す前にnullチェックを行います。

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

または、null条件演算子を使うと簡潔に書けます。

C#
message?.Invoke();

イベントやコールバックでは、登録されている処理がない可能性があるため、?.Invoke()を使う書き方がよく使われます。

C#
Completed?.Invoke();

9-3. ラムダ式の書き方を間違える

ラムダ式では、引数や戻り値がdelegateの型と一致している必要があります。

たとえば、次のFunc<int, int, int>は、intを2つ受け取り、intを返す処理を表します。

C#
Func<int, int, int> add = (a, b) => a + b;

次のように、戻り値がないラムダ式を代入するとエラーになります。

C#
Func<int, int, int> add = (a, b) =>
{
Console.WriteLine(a + b);
}; // エラー

Funcは戻り値が必要なので、returnを書く必要があります。

C#
Func<int, int, int> add = (a, b) =>
{
return a + b;
};

戻り値が不要な場合は、Actionを使います。

C#
Action<int, int> printSum = (a, b) =>
{
Console.WriteLine(a + b);
};

9-4. マルチキャストdelegateの戻り値で混乱する

戻り値があるdelegateに複数のメソッドを登録した場合、最終的に取得できる戻り値は最後のメソッドの戻り値です。

C#
delegate int NumberDelegate();

class Program
{
static void Main()
{
NumberDelegate number = GetOne;
number += GetTwo;

Console.WriteLine(number());
}

static int GetOne()
{
return 1;
}

static int GetTwo()
{
return 2;
}
}

実行結果は次の通りです。

2

GetOneも実行されますが、戻り値として受け取れるのはGetTwoの戻り値です。

そのため、マルチキャストdelegateでは、基本的に戻り値なしのvoidを使うほうが安全でわかりやすいです。

9-5. eventとdelegateを混同してしまう

delegateeventは似ていますが、役割が異なります。

delegateは、メソッドを参照するための型です。

C#
delegate void NotifyDelegate();

eventは、そのdelegateをイベントとして安全に公開するための仕組みです。

C#
public event NotifyDelegate Notified;

delegateだけを公開すると、外部から直接上書きや呼び出しができてしまいます。

C#
publisher.Notified = null;
publisher.Notified();

一方、eventを使うと、外部からは基本的に+=-=で登録・解除する形になります。

C#
publisher.Notified += Handler;
publisher.Notified -= Handler;

何かが起きたことを外部へ通知したい場合は、delegateを直接公開するのではなく、eventを使うのが一般的です。

10. C# delegateの実務での活用例

C#のdelegateは、実務でもさまざまな場面で使われています。

ただし、現代のC#では、独自delegateよりもActionFuncevent、ラムダ式、LINQの中で自然に使われることが多いです。

10-1. ボタンクリックなどのイベント処理

C#でGUIアプリケーションを作る場合、ボタンクリックなどのイベント処理でdelegateの考え方が使われます。

たとえば、Windows FormsやWPFでは、ボタンがクリックされたときの処理をイベントに登録します。

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

このButton_Clickメソッドは、イベントに登録されるメソッドです。

イベントの裏側ではdelegateの仕組みが使われており、「クリックされたらこのメソッドを呼ぶ」という形になっています。

10-2. 非同期処理や完了通知のコールバック

非同期処理や時間のかかる処理の完了通知にも、delegateの考え方が使われます。

たとえば、処理完了後に呼び出すメソッドをActionとして渡す例です。

C#
static void ProcessData(Action onCompleted)
{
Console.WriteLine("処理中...");

onCompleted?.Invoke();
}

static void Main()
{
ProcessData(() =>
{
Console.WriteLine("処理が完了しました");
});
}

実行結果は次の通りです。

処理中...
処理が完了しました

現在のC#では、非同期処理にはasyncawaitを使うことが多いですが、完了時に何かを実行するという考え方はdelegateやコールバックとつながっています。

10-3. 条件や処理を外部から渡す設計

実務では、条件や処理を外部から渡す設計でFuncActionがよく使われます。

たとえば、ユーザー一覧から条件に合うユーザーだけを取り出す例です。

C#
class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
}

static List<User> FilterUsers(List<User> users, Func<User, bool> condition)
{
List<User> result = new List<User>();

foreach (User user in users)
{
if (condition(user))
{
result.Add(user);
}
}

return result;
}

呼び出し側では、条件を自由に渡せます。

C#
List<User> adults = FilterUsers(users, user => user.Age >= 20);
List<User> minors = FilterUsers(users, user => user.Age < 20);

このように、条件を外部から渡せるようにすると、メソッドの再利用性が高まります。

10-4. LINQやコレクション処理との関係

C#のLINQでは、Funcが非常によく使われています。

たとえば、次のコードを見てみましょう。

C#
int[] numbers = { 1, 2, 3, 4, 5, 6 };

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

Whereメソッドに渡しているx => x % 2 == 0はラムダ式です。

そして、このラムダ式は内部的にはFunc<int, bool>のようなdelegateとして扱われます。

C#
Func<int, bool> isEven = x => x % 2 == 0;

var evenNumbers = numbers.Where(isEven);

つまり、LINQを使うときにも、delegateの考え方は登場しています。

Selectも同様です。

C#
var doubled = numbers.Select(x => x * 2);

このx => x * 2も、値を受け取って別の値を返す処理です。

このように、C#のコレクション処理を理解するうえでも、delegate、Func、ラムダ式の知識は重要です。

10-5. テストしやすいコードにするための活用

delegateFuncを使うと、処理を外部から差し替えられるため、テストしやすいコードにできます。

たとえば、現在日時を使う処理を考えます。

C#
class ReportService
{
public string CreateReport()
{
DateTime now = DateTime.Now;
return $"作成日時: {now}";
}
}

このコードは、実行するたびに現在時刻が変わるため、テストしづらい場合があります。

そこで、現在日時を取得する処理を外から渡せるようにします。

C#
class ReportService
{
private readonly Func<DateTime> _nowProvider;

public ReportService(Func<DateTime> nowProvider)
{
_nowProvider = nowProvider;
}

public string CreateReport()
{
DateTime now = _nowProvider();
return $"作成日時: {now}";
}
}

本番コードでは、現在時刻を返す処理を渡します。

C#
var service = new ReportService(() => DateTime.Now);

テストでは、固定日時を返す処理を渡せます。

C#
var service = new ReportService(() => new DateTime(2026, 1, 1));

このように、delegateやFuncを使うと、外部依存を差し替えやすくなり、テストしやすい設計にできます。

11. C# delegateに関するFAQ

ここでは、C#のdelegateについて初心者が疑問に思いやすいポイントをFAQ形式で解説します。

11-1. delegateは今でも使うべき?

はい、今でもdelegateの考え方は重要です。

ただし、実際のコードで毎回delegateキーワードを使って独自delegateを定義する機会は、以前より少なくなっています。

現在のC#では、次のような書き方がよく使われます。

C#
Action<string> logger = message => Console.WriteLine(message);
Func<int, int, int> add = (a, b) => a + b;

また、イベント処理ではeventEventHandlerがよく使われます。

C#
public event EventHandler? Completed;

つまり、delegateキーワード自体を書く機会は少なくても、ActionFunc、ラムダ式、event、LINQの理解にはdelegateの知識が欠かせません。

11-2. ActionとFuncだけ覚えれば十分?

簡単な処理であれば、ActionFuncを覚えるだけでも多くの場面に対応できます。

戻り値がない処理はActionです。

C#
Action<string> print = message => Console.WriteLine(message);

戻り値がある処理はFuncです。

C#
Func<int, int, int> add = (a, b) => a + b;

ただし、ActionFuncもdelegateの一種です。

そのため、なぜメソッドを変数に代入できるのか、なぜラムダ式を渡せるのかを理解するには、delegateの基本を知っておく必要があります。

また、業務上の意味を型名で表したい場合は、独自delegateも役立ちます。

C#
delegate bool OrderValidator(Order order);

11-3. delegateとインターフェースの違いは?

delegateは、主に1つの処理を渡したいときに使います。

C#
Func<int, int, int> calculate;

一方、インターフェースは、複数のメソッドやプロパティを持つまとまった振る舞いを表したいときに使います。

C#
interface ICalculator
{
int Add(int a, int b);
int Subtract(int a, int b);
}

簡単に言うと、次のように使い分けられます。

1つの処理だけを渡したい場合はdelegate、Action、Funcが向いています。

複数の処理をまとめて扱いたい場合はインターフェースが向いています。

たとえば、「数値を判定する条件を1つ渡したい」だけならFunc<int, bool>で十分です。

C#
Func<int, bool> condition = x => x > 0;

しかし、「保存、削除、検索などの機能をまとめて扱いたい」ならインターフェースのほうが適しています。

C#
interface IRepository
{
void Save();
void Delete();
void Find();
}

11-4. delegateはパフォーマンスに影響する?

delegateの呼び出しは通常のメソッド呼び出しに比べるとわずかなオーバーヘッドがあります。

ただし、一般的なアプリケーション開発では、ほとんどの場合大きな問題にはなりません。

たとえば、イベント処理、コールバック、LINQ、条件の差し替えなどでdelegateを使うことは一般的です。

パフォーマンスを気にするべきなのは、非常に大量のループ内で何百万回、何千万回もdelegateを呼び出すようなケースです。

通常は、パフォーマンスよりもコードの読みやすさ、保守性、柔軟性を優先して問題ありません。

ただし、処理速度が重要な箇所では、実測して判断することが大切です。

11-5. 初心者はどこまで理解すればよい?

初心者は、まず次のポイントを理解すれば十分です。

delegateはメソッドを変数のように扱う仕組みです。

C#
delegate void MessageDelegate();

delegateには、型が一致するメソッドを代入できます。

C#
MessageDelegate message = SayHello;

delegateは通常のメソッドのように呼び出せます。

C#
message();

戻り値なしの処理にはAction、戻り値ありの処理にはFuncを使えます。

C#
Action<string> print = Console.WriteLine;
Func<int, int, int> add = (a, b) => a + b;

イベントはdelegateを安全に扱う仕組みです。

C#
public event EventHandler? Completed;

最初からマルチキャストdelegateやイベントの詳細まで完璧に理解する必要はありません。

まずは、「処理を変数に入れる」「処理を引数として渡す」「ActionとFuncはdelegateの便利版」という感覚をつかむことが大切です。

まとめ

C#のdelegateは、メソッドを変数のように扱うための仕組みです。

通常、メソッドは直接呼び出しますが、delegateを使うことで、メソッドを変数に代入したり、別のメソッドに引数として渡したり、あとから処理を差し替えたりできます。

基本構文は次の通りです。

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

たとえば、戻り値なし、引数なしのdelegateは次のように定義します。

C#
delegate void MessageDelegate();

戻り値あり、引数ありの場合は次のように書きます。

C#
delegate int CalculateDelegate(int a, int b);

実務では、独自delegateだけでなく、ActionFuncもよく使われます。

戻り値がない処理にはActionを使います。

C#
Action<string> print = message => Console.WriteLine(message);

戻り値がある処理にはFuncを使います。

C#
Func<int, int, int> add = (a, b) => a + b;

また、ラムダ式を使うことで、delegateに渡す処理を簡潔に書けます。

C#
Func<int, int, int> add = (a, b) => a + b;

さらに、C#のeventはdelegateを安全に扱うための仕組みです。

C#
public event EventHandler? Completed;

delegateを理解すると、イベント処理、コールバック、LINQ、ラムダ式、非同期処理など、C#でよく使われる機能の理解が深まります。

初心者は、まず次の3つを押さえておきましょう。

delegateはメソッドを変数のように扱う仕組み
Actionは戻り値なしのdelegate
Funcは戻り値ありのdelegate

C#のdelegateは最初は難しく感じるかもしれませんが、基本の考え方は「処理を渡せるようにする」というシンプルなものです。

サンプルコードを実際に動かしながら、少しずつ使い方に慣れていきましょう。