C# delegateとは?使い方・Action/Funcとの違い・ラムダ式まで初心者向けに徹底解説

はじめに

C#を学び始めると、delegateというキーワードに出会うことがあります。読み方は「デリゲート」です。検索では「csharp delegate」「C# delegateとは」「delegate 使い方」などで調べる人が多く、特に初心者にとっては少し理解しづらい概念のひとつです。

delegateは簡単にいうと、メソッドを変数のように扱うための仕組みです。

通常、メソッドは次のように直接呼び出します。

C#
Hello();

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

一方、delegateを使うと、呼び出すメソッドを変数に入れておき、あとから実行できます。

C#
MyDelegate action = Hello;
action();

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

delegate void MyDelegate();

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

この記事では、C#のdelegateについて、基本的な書き方から、ActionFuncとの違い、ラムダ式、マルチキャストdelegate、eventとの関係まで、初心者向けに順番に解説します。

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

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

C#のdelegateは、特定の形を持つメソッドを参照できる型です。

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

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

このメソッドを直接呼び出すなら、次のように書きます。

C#
SayHello();

しかし、delegateを使うと、このメソッドを変数に代入できます。

C#
MessageDelegate message = SayHello;
message();

delegate void MessageDelegate();

messageという変数にSayHelloメソッドを入れて、message()で実行しています。

つまり、delegateは「どのメソッドを実行するか」をあとから決めたいときに便利です。

1-2. delegateでできること:メソッドの代入・受け渡し・呼び出し

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

できること内容
メソッドを代入するdelegate変数にメソッドを入れられる
メソッドを引数として渡す別のメソッドに処理内容を渡せる
メソッドをあとから呼び出す登録されたメソッドをdelegate経由で実行できる
処理を差し替える条件に応じて実行する処理を変更できる
複数のメソッドをまとめて呼ぶマルチキャストdelegateとして使える

たとえば、処理の途中で「完了したらこのメソッドを呼んでほしい」という場面があります。このような仕組みをコールバックと呼びます。

C#
void DoWork(Callback callback)
{
Console.WriteLine("処理中...");
callback();
}

void Finished()
{
Console.WriteLine("処理が完了しました");
}

DoWork(Finished);

delegate void Callback();

この例では、DoWorkメソッドにFinishedメソッドを渡しています。DoWork側は、処理が終わったタイミングで渡されたメソッドを呼び出しています。

1-3. なぜdelegateが必要なのか?普通のメソッド呼び出しとの違い

普通のメソッド呼び出しでは、呼び出すメソッドがコード上で固定されています。

C#
SendEmail();

この場合、常にSendEmailが実行されます。

一方、delegateを使うと、実行するメソッドをあとから変更できます。

C#
Notify notify;

if (useEmail)
{
notify = SendEmail;
}
else
{
notify = SendSms;
}

notify();

void SendEmail()
{
Console.WriteLine("メールで通知します");
}

void SendSms()
{
Console.WriteLine("SMSで通知します");
}

delegate void Notify();

このように、条件によって処理を切り替えたい場合にdelegateは便利です。

普通のメソッド呼び出しは「この処理を実行する」と決め打ちします。
delegateは「実行する処理を変数として扱い、必要に応じて差し替える」ことができます。

1-4. delegateを理解するために押さえたい「型」と「シグネチャ」

delegateを理解するうえで重要なのが、シグネチャです。

シグネチャとは、簡単にいうとメソッドの形です。特に次の要素が重要です。

  • 引数の数

  • 引数の型

  • 戻り値の型

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

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

このCalculate delegateに代入できるのは、次のような形のメソッドです。

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

戻り値がintで、引数がintを2つ持っているため、代入できます。

C#
Calculate calc = Add;
Console.WriteLine(calc(3, 5)); // 8

一方、次のようなメソッドは代入できません。

C#
void Show(int x, int y)
{
Console.WriteLine(x + y);
}

戻り値がvoidなので、intを返すCalculateには合いません。

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

2. C# delegateの基本的な書き方と使い方

2-1. delegate型を宣言する基本構文

C#でdelegate型を宣言する基本構文は次のとおりです。

C#
delegate 戻り値の型 delegate名(引数);

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

C#
delegate void MyDelegate();

引数あり・戻り値なしの場合は次のようになります。

C#
delegate void MessageDelegate(string message);

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

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

delegateは、メソッドそのものを定義しているわけではありません。
「この形のメソッドを入れられる型」を定義しています。

2-2. delegate変数にメソッドを代入する

delegate型を定義したら、その型の変数にメソッドを代入できます。

C#
Greeting greeting = SayHello;
greeting();

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

delegate void Greeting();

Greetingはdelegate型です。
greetingはdelegate型の変数です。
SayHelloは実際に代入されるメソッドです。

重要なのは、SayHelloを書くときに()を付けないことです。

C#
Greeting greeting = SayHello;   // 正しい
Greeting greeting = SayHello(); // 間違い

SayHello()と書くと、その場でメソッドを実行する意味になります。delegateに代入するときは、メソッドそのものを渡すため、()は付けません。

2-3. delegate経由でメソッドを呼び出す

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

C#
Greeting greeting = SayHello;
greeting();

void SayHello()
{
Console.WriteLine("Hello");
}

delegate void Greeting();

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

C#
greeting.Invoke();

次の2つはほぼ同じ意味です。

C#
greeting();
greeting.Invoke();

ただし、delegate変数がnullの可能性がある場合は注意が必要です。安全に呼び出すには、null条件演算子を使います。

C#
greeting?.Invoke();

2-4. 引数あり・戻り値ありのdelegateサンプル

次に、引数と戻り値があるdelegateを見てみましょう。

C#
Calculate calc = Add;

int result = calc(10, 20);
Console.WriteLine(result); // 30

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

delegate int Calculate(int a, int b);

この例では、Calculate delegateは「intを2つ受け取り、intを返すメソッド」を扱えます。

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

C#
int Subtract(int x, int y)
{
return x - y;
}

使うメソッドを差し替えると、同じdelegate変数でも処理内容を変更できます。

C#
Calculate calc;

calc = Add;
Console.WriteLine(calc(10, 5)); // 15

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

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

int Subtract(int x, int y)
{
return x - y;
}

delegate int Calculate(int a, int b);

2-5. staticメソッドとインスタンスメソッドをdelegateに代入する違い

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

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

C#
Message message = Printer.Print;
message("Hello");

class Printer
{
public static void Print(string text)
{
Console.WriteLine(text);
}
}

delegate void Message(string text);

staticメソッドはクラス名から直接参照できます。

C#
Printer.Print

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

C#
Printer printer = new Printer();

Message message = printer.Print;
message("Hello");

class Printer
{
public void Print(string text)
{
Console.WriteLine(text);
}
}

delegate void Message(string text);

インスタンスメソッドの場合は、先にオブジェクトを作成し、そのオブジェクトのメソッドを代入します。

C#
printer.Print

違いをまとめると次のようになります。

種類代入方法特徴
staticメソッドClassName.MethodNameインスタンス不要
インスタンスメソッドobject.MethodNameオブジェクトが必要

3. delegateの具体的な使用例

3-1. コールバック処理でdelegateを使う

delegateの代表的な使い方が、コールバック処理です。

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

C#
void Download(string url, CompletedCallback callback)
{
Console.WriteLine($"{url} からダウンロード中...");
Console.WriteLine("ダウンロード完了");

callback();
}

void ShowCompletedMessage()
{
Console.WriteLine("完了メッセージを表示します");
}

Download("https://example.com/file.zip", ShowCompletedMessage);

delegate void CompletedCallback();

Downloadメソッドは、ダウンロード完了後にcallback()を呼び出しています。
どんな処理を完了後に行うかは、呼び出し元が決められます。

3-2. 条件によって実行する処理を切り替える

delegateを使うと、条件に応じて実行する処理を切り替えられます。

C#
DiscountCalculator calculator;

string memberRank = "Gold";

if (memberRank == "Gold")
{
calculator = GoldDiscount;
}
else
{
calculator = NormalDiscount;
}

int price = calculator(1000);
Console.WriteLine(price);

int GoldDiscount(int price)
{
return price - 200;
}

int NormalDiscount(int price)
{
return price - 50;
}

delegate int DiscountCalculator(int price);

この例では、会員ランクによって割引計算の処理を切り替えています。

普通にif文の中で直接計算してもよいですが、delegateを使うと「計算方法」を変数として扱えるため、処理を整理しやすくなります。

3-3. メソッドを引数として渡して処理を共通化する

delegateを使うと、処理の一部だけを外から渡して、共通処理を作れます。

C#
void ProcessNumbers(int[] numbers, NumberProcessor processor)
{
foreach (int number in numbers)
{
processor(number);
}
}

void PrintDouble(int number)
{
Console.WriteLine(number * 2);
}

void PrintSquare(int number)
{
Console.WriteLine(number * number);
}

int[] values = { 1, 2, 3 };

ProcessNumbers(values, PrintDouble);
ProcessNumbers(values, PrintSquare);

delegate void NumberProcessor(int number);

ProcessNumbersは、配列を順番に処理する共通部分を担当しています。
実際に何をするかは、processorとして渡されたメソッドが決めます。

この考え方は、LINQのWhereSelectにもつながります。

3-4. イベント処理でdelegateが使われる理由

C#のイベント処理では、delegateが重要な役割を持っています。

たとえば、ボタンがクリックされたときに処理を実行する場面を考えてみましょう。

C#
button.Click += Button_Click;

このButton_Clickは、クリックイベントが発生したときに呼ばれるメソッドです。

イベントは、「何かが起きたときに、登録されているメソッドを呼び出す」仕組みです。
この「登録されているメソッドを呼び出す」部分でdelegateが使われます。

簡単な例で見ると、次のようになります。

C#
class Button
{
public event ClickHandler? Click;

public void Press()
{
Click?.Invoke();
}
}

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

Button button = new Button();
button.Click += OnClick;

button.Press();

delegate void ClickHandler();

button.Press()が呼ばれると、登録されているOnClickが実行されます。

3-5. LINQや非同期処理の理解にもdelegateが役立つ理由

C#では、delegateそのものを直接書く機会は昔より減っています。
しかし、delegateの考え方は今でも非常に重要です。

たとえば、LINQでは次のようにラムダ式をよく使います。

C#
var numbers = new[] { 1, 2, 3, 4, 5 };

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

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

また、非同期処理でも、完了後に実行する処理や、タスクに渡す処理としてdelegateの考え方が登場します。

C#
Task.Run(() =>
{
Console.WriteLine("別スレッドで処理します");
});

このラムダ式も、処理を値として渡している例です。

delegateを理解すると、C#でよく見る「メソッドを渡す」「処理を渡す」「ラムダ式を書く」というコードの意味がわかりやすくなります。

4. delegateとAction/Funcの違い

4-1. Actionとは?戻り値がない処理を表す定義済みdelegate

Actionは、C#にあらかじめ用意されているdelegateです。
戻り値がない処理、つまりvoidを返すメソッドを扱うときに使います。

たとえば、次のようなdelegateを自分で定義したとします。

C#
delegate void MessageAction(string message);

これは、Action<string>で置き換えられます。

C#
Action<string> messageAction = ShowMessage;

messageAction("こんにちは");

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

Action<string>は、「stringを1つ受け取り、戻り値がない処理」を表します。

引数なしの場合は、単にActionと書きます。

C#
Action action = SayHello;
action();

void SayHello()
{
Console.WriteLine("Hello");
}

複数の引数も指定できます。

C#
Action<string, int> printUser = PrintUser;

printUser("田中", 25);

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

4-2. Funcとは?戻り値がある処理を表す定義済みdelegate

FuncもC#にあらかじめ用意されているdelegateです。
Funcは、戻り値がある処理を表します。

たとえば、次のようなdelegateを自分で定義したとします。

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

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

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

int result = calculator(3, 5);
Console.WriteLine(result); // 8

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

Func<int, int, int>の最後のintが戻り値の型です。

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

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

C#
Func<int, bool> isEven = IsEven;

Console.WriteLine(isEven(4)); // True

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

引数なしで戻り値だけがある場合は、次のように書きます。

C#
Func<string> getMessage = GetMessage;

Console.WriteLine(getMessage());

string GetMessage()
{
return "Hello";
}

4-3. delegate・Action・Funcの使い分け早見表

delegateActionFuncの使い分けをまとめると次のようになります。

種類用途戻り値
delegate独自の名前を持つ処理型を定義したい任意delegate int Calculator(int x, int y);
Action戻り値がない処理を簡潔に表したいvoidAction<string>
Func戻り値がある処理を簡潔に表したいありFunc<int, int, int>
Predicate条件判定を表したいboolPredicate<int>

初心者のうちは、次のように考えるとわかりやすいです。

  • 戻り値がないならAction

  • 戻り値があるならFunc

  • 条件判定ならPredicateまたはFunc<T, bool>

  • 意味のある名前を付けたいなら独自のdelegate

4-4. 自分でdelegateを定義するべきケース

ActionFuncがあるなら、自分でdelegateを定義する必要はないように見えるかもしれません。

しかし、独自delegateを定義したほうがよい場面もあります。

たとえば、次のようなコードを見てください。

C#
delegate bool UserValidator(User user);

このdelegate名を見ると、「ユーザーを検証する処理」だとすぐにわかります。

一方、Func<User, bool>と書くと、型としては同じような意味ですが、何を表しているのかは名前だけではわかりにくくなります。

C#
Func<User, bool> validator;

独自delegateを使うメリットは、処理の意味を名前で表現できることです。

次のようなケースでは、自分でdelegateを定義するとよいでしょう。

  • ライブラリやフレームワークの公開APIとして使う

  • 処理の意味を明確にしたい

  • イベントやコールバックの役割を名前で表したい

  • 同じシグネチャでも意味が異なる処理を区別したい

4-5. Action/Funcを使ったほうがよいケース

一方で、短い処理やローカルな処理では、ActionFuncを使ったほうが簡潔です。

C#
Action<string> log = message =>
{
Console.WriteLine($"LOG: {message}");
};

log("処理開始");

わざわざ次のようにdelegateを定義する必要がない場合も多いです。

C#
delegate void LogHandler(string message);

ActionFuncが向いているのは、次のような場面です。

  • 一時的な処理を渡したい

  • ラムダ式と組み合わせたい

  • LINQのように短い条件や変換処理を書きたい

  • 独自の名前を付けるほどではない

  • コードを簡潔にしたい

実務では、独自delegateよりもActionFuncをよく見かける場面が多いです。

4-6. Predicateとの違いもあわせて理解する

Predicate<T>は、条件判定を表すdelegateです。

C#
Predicate<int> isEven = number => number % 2 == 0;

Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(5)); // False

Predicate<int>は、次のような意味です。

C#
intを受け取り、boolを返す処理

これは、Func<int, bool>とほぼ同じように使えます。

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

違いは、名前から用途がわかりやすいかどうかです。

意味
Predicate<T>Tを受け取って条件判定する
Func<T, bool>Tを受け取ってboolを返す

条件判定であることを明確にしたい場合はPredicate<T>、LINQや一般的な関数として扱う場合はFunc<T, bool>がよく使われます。

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

5-1. ラムダ式とは?delegateを短く書くための構文

ラムダ式は、delegateに代入する処理を短く書くための構文です。

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

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

これをdelegateに代入するには、次のように書けます。

C#
Calculate calc = Add;

しかし、わざわざAddメソッドを定義せず、その場で処理を書きたい場合があります。
そのときにラムダ式を使います。

C#
Calculate calc = (x, y) => x + y;

ラムダ式の基本形は次のとおりです。

C#
(引数) => 処理

=>は「ラムダ演算子」と呼ばれます。
左側に引数、右側に処理を書きます。

5-2. delegateにラムダ式を代入する基本例

独自delegateにラムダ式を代入する例を見てみましょう。

C#
Calculate add = (x, y) => x + y;

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

delegate int Calculate(int x, int y);

複数行の処理を書きたい場合は、ブロック { } を使います。

C#
Calculate add = (x, y) =>
{
int result = x + y;
return result;
};

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

delegate int Calculate(int x, int y);

戻り値がないdelegateの場合は、次のように書きます。

C#
Message message = text =>
{
Console.WriteLine(text);
};

message("こんにちは");

delegate void Message(string text);

引数が1つの場合、型が推論できるなら丸かっこを省略できます。

C#
Message message = text => Console.WriteLine(text);

5-3. 匿名メソッドとラムダ式の違い

ラムダ式が登場する前から、C#には匿名メソッドという書き方がありました。

匿名メソッドは次のように書きます。

C#
Message message = delegate(string text)
{
Console.WriteLine(text);
};

message("Hello");

delegate void Message(string text);

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

C#
Message message = text => Console.WriteLine(text);

どちらも「名前のないメソッドをdelegateに代入する」ための書き方です。

違いをまとめると次のようになります。

種類書き方特徴
匿名メソッドdelegate(...) { ... }古い書き方。明示的
ラムダ式(...) => ...短く書ける。現在よく使われる

現在のC#では、匿名メソッドよりもラムダ式を使うことが一般的です。

5-4. Action/Funcとラムダ式を組み合わせた実践例

ActionFuncは、ラムダ式と非常に相性がよいです。

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

C#
Action<string> log = message =>
{
Console.WriteLine($"[LOG] {message}");
};

log("処理を開始しました");

戻り値がある処理ならFuncを使います。

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

Console.WriteLine(add(3, 7)); // 10

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

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

Console.WriteLine(isEven(10)); // True

配列やリストの処理でも活躍します。

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

List<int> evenNumbers = numbers
.Where(number => number % 2 == 0)
.ToList();

foreach (int number in evenNumbers)
{
Console.WriteLine(number);
}

このnumber => number % 2 == 0も、delegateとして渡されている処理です。

5-5. ラムダ式でdelegateを書くときの注意点

ラムダ式は便利ですが、いくつか注意点があります。

まず、delegateのシグネチャに合っていないラムダ式は代入できません。

C#
Func<int, int> square = x => x * x; // OK

これは、intを受け取り、intを返すので問題ありません。

一方、次のようなコードはエラーになります。

C#
Func<int, int> action = x => Console.WriteLine(x); // エラー

Console.WriteLineは戻り値がvoidなので、intを返すFunc<int, int>には合いません。

この場合はAction<int>を使います。

C#
Action<int> action = x => Console.WriteLine(x);

また、複数行のラムダ式で戻り値が必要な場合は、returnを書き忘れないようにしましょう。

C#
Func<int, int> square = x =>
{
return x * x;
};

次のようにreturnがないとエラーになります。

C#
Func<int, int> square = x =>
{
x * x; // エラー
};

ラムダ式では、引数の型や戻り値の型がdelegateから推論されます。
そのため、どのdelegateに代入しているかを意識することが重要です。

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

6-1. 複数のメソッドをdelegateに登録する仕組み

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

C#
Notify notify = SendEmail;
notify += SendSms;

notify();

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

void SendSms()
{
Console.WriteLine("SMSを送信しました");
}

delegate void Notify();

このコードを実行すると、SendEmailSendSmsの両方が呼び出されます。

メールを送信しました
SMSを送信しました

1つのdelegate変数に複数のメソッドを登録できるため、通知処理やイベント処理でよく使われます。

6-2. +=と-=でメソッドを追加・削除する

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

C#
notify += SendEmail;
notify += SendSms;

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

C#
notify -= SendSms;

例を見てみましょう。

C#
Notify notify = SendEmail;
notify += SendSms;

Console.WriteLine("1回目");
notify();

notify -= SendSms;

Console.WriteLine("2回目");
notify();

void SendEmail()
{
Console.WriteLine("メール通知");
}

void SendSms()
{
Console.WriteLine("SMS通知");
}

delegate void Notify();

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

1回目
メール通知
SMS通知
2回目
メール通知

SendSmsを削除したため、2回目はSendEmailだけが実行されます。

6-3. マルチキャストdelegateの呼び出し順序

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

C#
Notify notify = First;
notify += Second;
notify += Third;

notify();

void First()
{
Console.WriteLine("1番目");
}

void Second()
{
Console.WriteLine("2番目");
}

void Third()
{
Console.WriteLine("3番目");
}

delegate void Notify();

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

1番目
2番目
3番目

ただし、登録されたメソッドのどれかで例外が発生すると、その時点で処理が止まります。

C#
Notify notify = First;
notify += Error;
notify += Third;

notify();

void First()
{
Console.WriteLine("1番目");
}

void Error()
{
throw new Exception("エラーが発生しました");
}

void Third()
{
Console.WriteLine("3番目");
}

delegate void Notify();

この場合、Thirdは実行されません。
必要であれば、個別に例外処理をする設計が必要です。

6-4. 戻り値があるdelegateで注意すべきポイント

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

C#
Calculate calc = Add;
calc += Multiply;

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

int Add(int x, int y)
{
Console.WriteLine("Add");
return x + y;
}

int Multiply(int x, int y)
{
Console.WriteLine("Multiply");
return x * y;
}

delegate int Calculate(int x, int y);

この場合、AddMultiplyの両方が呼ばれます。
しかし、resultに入るのは最後に実行されたMultiplyの戻り値です。

Add
Multiply
15

つまり、マルチキャストdelegateで戻り値を使う場合、最後のメソッドの戻り値だけが取得されます。

複数の戻り値をすべて扱いたい場合は、GetInvocationList()を使って個別に呼び出す方法があります。

C#
foreach (Calculate method in calc.GetInvocationList())
{
int value = method(3, 5);
Console.WriteLine(value);
}

戻り値が必要な処理では、マルチキャストdelegateの使い方に注意しましょう。

6-5. nullチェックと安全な呼び出し方法

delegate変数に何も代入されていない場合、その値はnullです。

C#
Notify? notify = null;
notify(); // エラー

nullのdelegateを呼び出すと、NullReferenceExceptionが発生します。

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

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

現在のC#では、null条件演算子を使う書き方がよく使われます。

C#
notify?.Invoke();

これは、「notifyがnullでなければ呼び出す」という意味です。

イベント処理でもよく使われます。

C#
Click?.Invoke();

delegateやeventを扱うときは、nullの可能性を意識しておくことが大切です。

7. delegateとeventの違い

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

eventは、delegateをもとにした仕組みです。
簡単にいうと、event外部から勝手に呼び出されたり、上書きされたりしないように制限されたdelegateです。

delegateだけを使うと、外部から次のような操作ができてしまいます。

  • メソッドを追加する

  • メソッドを削除する

  • delegateを丸ごと上書きする

  • 外部から直接呼び出す

イベントとして使う場合、外部から勝手に呼び出せると問題があります。
そこでeventを使って、安全に扱えるようにします。

7-2. delegateだけでイベント処理を書く場合の問題点

delegateだけでイベントのような処理を書くと、次のようになります。

C#
class Button
{
public ClickHandler? Click;
}

Button button = new Button();

button.Click += OnClick;
button.Click?.Invoke();

void OnClick()
{
Console.WriteLine("クリックされました");
}

delegate void ClickHandler();

このコードでは、クラスの外側からbutton.Click?.Invoke()を実行できてしまいます。

つまり、ボタンが実際に押されていなくても、外部からクリックイベントを発生させられるということです。

さらに、外部からdelegateを丸ごと上書きすることもできます。

C#
button.Click = null;

これでは、登録されていたイベント処理がすべて消えてしまいます。

7-3. eventを使うと外部からできること・できないこと

eventを使うと、外部からできる操作が制限されます。

C#
class Button
{
public event ClickHandler? Click;

public void Press()
{
Click?.Invoke();
}
}

Button button = new Button();

button.Click += OnClick;
button.Press();

void OnClick()
{
Console.WriteLine("クリックされました");
}

delegate void ClickHandler();

eventにすると、外部からできるのは基本的に次の操作です。

C#
button.Click += OnClick;
button.Click -= OnClick;

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

C#
button.Click = null;      // エラー
button.Click?.Invoke(); // エラー

イベントを発生させることができるのは、そのeventを定義しているクラスの内部だけです。

これにより、イベントの発生タイミングをクラス側で安全に管理できます。

7-4. C#のイベント処理でよく見るEventHandlerとは

C#のイベント処理では、EventHandlerという定義済みdelegateをよく見かけます。

EventHandlerは、一般的なイベントで使われるdelegateです。

C#
class Button
{
public event EventHandler? Click;

public void Press()
{
Click?.Invoke(this, EventArgs.Empty);
}
}

Button button = new Button();

button.Click += Button_Click;
button.Press();

void Button_Click(object? sender, EventArgs e)
{
Console.WriteLine("クリックされました");
}

EventHandlerの形は、基本的に次のようになっています。

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

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

独自のイベント情報を渡したい場合は、EventArgsを継承したクラスを使います。

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

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

class Notifier
{
public event EventHandler<MessageEventArgs>? Notified;

public void Notify(string message)
{
Notified?.Invoke(this, new MessageEventArgs(message));
}
}

Notifier notifier = new Notifier();

notifier.Notified += (sender, e) =>
{
Console.WriteLine(e.Message);
};

notifier.Notify("通知が届きました");

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

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

使いたいこと適した仕組み
メソッドを変数として扱いたいdelegate
処理を引数として渡したいdelegate / Action / Func
コールバックを実装したいdelegate / Action / Func
何かが起きたことを通知したいevent
外部から登録だけ許可したいevent
外部から勝手に呼び出されたくないevent

イベント処理として使うなら、基本的にはeventを使います。
単に処理を渡したいだけなら、delegateActionFuncで十分です。

8. delegateでよくあるエラーとつまずきポイント

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

delegateで最も多いエラーのひとつが、引数や戻り値の型が一致しないことです。

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

void Add(int x, int y)
{
Console.WriteLine(x + y);
}

Calculator calc = Add; // エラー

Calculatorintを返す必要があります。
しかし、Addvoidなので代入できません。

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

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

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

Calculator calc = Add;

delegateにメソッドを代入するときは、次の点を確認しましょう。

  • 引数の数が一致しているか

  • 引数の型が一致しているか

  • 戻り値の型が一致しているか

  • voidと戻り値ありを混同していないか

8-2. delegateに何も代入されておらずnullになる

delegate変数は、何も代入されていないとnullです。

C#
Action? action = null;

action(); // NullReferenceException

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

C#
action?.Invoke();

または、明示的にnullチェックします。

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

特にeventでは、誰もイベントハンドラーを登録していない可能性があります。
そのため、次のように安全に呼び出す書き方がよく使われます。

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

8-3. ラムダ式の書き方でコンパイルエラーになる

ラムダ式では、delegateの型に合った処理を書く必要があります。

C#
Func<int, int> square = x => x * x;

これは問題ありません。

しかし、次のようなコードはエラーになります。

C#
Func<int, int> square = x =>
{
x * x;
};

ブロック形式のラムダ式で戻り値が必要な場合は、returnが必要です。

C#
Func<int, int> square = x =>
{
return x * x;
};

また、引数が複数ある場合は丸かっこが必要です。

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

次のように書くとエラーになります。

C#
Func<int, int, int> add = x, y => x + y; // エラー

ラムダ式のエラーは、書き方そのものよりも「代入先のdelegate型と合っているか」を確認すると解決しやすくなります。

8-4. ActionとFuncの選び方を間違える

ActionFuncの違いで迷う初心者は多いです。

戻り値がないならActionです。

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

戻り値があるならFuncです。

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

次のように、値を返したいのにActionを使うとエラーになります。

C#
Action<int, int> add = (x, y) => x + y; // エラー

Actionは戻り値を返せません。

正しくはFuncです。

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

判断に迷ったら、まず「戻り値が必要か」を考えましょう。

8-5. 非同期処理や例外処理でdelegateを使うときの注意点

delegateに非同期処理を渡す場合は、asyncと戻り値に注意が必要です。

たとえば、非同期処理を表す場合はFunc<Task>を使うことが多いです。

C#
Func<Task> asyncAction = async () =>
{
await Task.Delay(1000);
Console.WriteLine("非同期処理が完了しました");
};

await asyncAction();

戻り値がある非同期処理なら、Func<Task<T>>を使います。

C#
Func<Task<int>> getNumberAsync = async () =>
{
await Task.Delay(1000);
return 100;
};

int result = await getNumberAsync();

注意したいのは、async voidです。

C#
Action action = async () =>
{
await Task.Delay(1000);
throw new Exception("エラー");
};

このような書き方は、例外の扱いが難しくなる場合があります。
イベントハンドラーなど一部の場面を除き、非同期処理ではFunc<Task>を使うほうが安全です。

C#
Func<Task> action = async () =>
{
await Task.Delay(1000);
};

delegateを使うときは、例外がどこで発生し、どこで捕捉されるのかも意識しましょう。

9. delegateを使うべき場面・使わなくてよい場面

9-1. delegateが向いている場面

delegateが向いているのは、処理そのものを柔軟に差し替えたい場面です。

具体的には、次のようなケースです。

場面
コールバック処理完了後に呼ぶメソッドを渡す
イベント処理何かが起きたときに登録済み処理を呼ぶ
処理の切り替え条件によって実行するメソッドを変える
共通処理化一部の処理だけ外から渡す
ラムダ式の利用短い処理をその場で渡す

たとえば、ログ出力の方法を外から渡したい場合にdelegateは便利です。

C#
void Execute(Action<string> logger)
{
logger("処理開始");
logger("処理終了");
}

Execute(message => Console.WriteLine(message));

9-2. Action/Funcで十分な場面

実務では、独自delegateを定義しなくても、ActionFuncで十分な場面が多くあります。

C#
void Process(Action action)
{
Console.WriteLine("前処理");
action();
Console.WriteLine("後処理");
}

Process(() => Console.WriteLine("メイン処理"));

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

C#
int Execute(Func<int> operation)
{
Console.WriteLine("計算します");
return operation();
}

int result = Execute(() => 10 + 20);

短い処理を渡すだけなら、独自delegateよりもActionFuncのほうが読みやすい場合があります。

9-3. インターフェースを使ったほうがよい場面

delegateは便利ですが、すべての場面に向いているわけではありません。

複数のメソッドをまとめて扱いたい場合は、インターフェースのほうが適しています。

たとえば、支払い処理を考えてみましょう。

C#
interface IPaymentService
{
void Pay(int amount);
void Cancel();
void Refund(int amount);
}

支払いには、Payだけでなく、CancelRefundなど複数の操作が必要かもしれません。

このような場合、delegateで個別にメソッドを渡すよりも、インターフェースとして設計したほうが自然です。

delegateが向いているのは、基本的に「単一の処理」を渡したい場合です。
複数の関連する振る舞いをまとめたい場合は、クラスやインターフェースを検討しましょう。

9-4. 初心者がdelegateを学ぶメリット

初心者がdelegateを学ぶメリットは非常に大きいです。

なぜなら、delegateを理解すると、C#のさまざまな機能がつながって見えるようになるからです。

たとえば、次のようなコードを見たときに理解しやすくなります。

C#
numbers.Where(x => x > 10);
C#
button.Click += Button_Click;
C#
Task.Run(() => DoWork());

これらはすべて、「処理を渡す」という考え方が関係しています。

delegateを理解すると、次の内容も学びやすくなります。

  • ラムダ式

  • 匿名メソッド

  • Action

  • Func

  • Predicate

  • LINQ

  • event

  • 非同期処理

  • コールバック

  • DIやテストでの処理差し替え

delegateは、C#の中級者に進むための重要な基礎です。

9-5. 実務ではdelegateをどのように見かけるのか

実務では、delegateキーワードを直接書く機会はそれほど多くないかもしれません。

しかし、delegateの考え方は頻繁に登場します。

たとえば、LINQではラムダ式を渡します。

C#
var activeUsers = users.Where(user => user.IsActive);

イベント処理では、イベントハンドラーを登録します。

C#
button.Click += Button_Click;

非同期処理では、処理をラムダ式で渡します。

C#
await Task.Run(() => HeavyProcess());

また、メソッドの引数としてActionFuncを受け取る設計もよくあります。

C#
void Retry(Action action)
{
try
{
action();
}
catch
{
Console.WriteLine("失敗しました");
}
}

このように、実務ではdelegateそのものよりも、ActionFunc、ラムダ式、eventを通してdelegateに触れることが多いです。

10. C# delegateの理解を深める練習問題

10-1. delegateで計算処理を切り替える

まずは、delegateを使って計算処理を切り替えてみましょう。

C#
Calculator calc;

calc = Add;
Console.WriteLine(calc(10, 5)); // 15

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

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

int Subtract(int x, int y)
{
return x - y;
}

delegate int Calculator(int x, int y);

この練習では、同じCalculator型の変数に、異なるメソッドを代入しています。

余裕があれば、掛け算や割り算も追加してみましょう。

C#
int Multiply(int x, int y)
{
return x * y;
}
C#
calc = Multiply;
Console.WriteLine(calc(10, 5)); // 50

10-2. Actionでログ出力処理を渡す

次は、Actionを使ってログ出力処理を渡す練習です。

C#
void Execute(Action<string> logger)
{
logger("処理を開始します");

Console.WriteLine("メイン処理中...");

logger("処理を終了します");
}

Execute(message => Console.WriteLine($"[LOG] {message}"));

Action<string>は、stringを受け取り、戻り値がない処理を表します。

ログの出力先を変えることもできます。

C#
Execute(message => Console.WriteLine($"[DEBUG] {DateTime.Now}: {message}"));

このように、処理の中身を外から渡すことで、共通処理を柔軟に使えます。

10-3. Funcで条件に応じた値を返す

Funcを使って、条件に応じた値を返す練習です。

C#
int GetPrice(int basePrice, Func<int, int> discount)
{
return discount(basePrice);
}

int goldPrice = GetPrice(1000, price => price - 200);
int normalPrice = GetPrice(1000, price => price - 50);

Console.WriteLine(goldPrice); // 800
Console.WriteLine(normalPrice); // 950

Func<int, int>は、intを受け取り、intを返す処理です。

さらに、会員ランクによって処理を切り替えることもできます。

C#
string rank = "Gold";

Func<int, int> discount = rank switch
{
"Gold" => price => price - 200,
"Silver" => price => price - 100,
_ => price => price - 50
};

Console.WriteLine(discount(1000));

10-4. ラムダ式を使ってコードを短く書き換える

次のdelegateとメソッドを、ラムダ式で短く書き換えてみましょう。

C#
delegate int Converter(int value);

int Double(int value)
{
return value * 2;
}

Converter converter = Double;

Console.WriteLine(converter(10));

ラムダ式を使うと、次のように書けます。

C#
Converter converter = value => value * 2;

Console.WriteLine(converter(10));

delegate int Converter(int value);

Funcを使うと、さらにdelegate宣言も省略できます。

C#
Func<int, int> converter = value => value * 2;

Console.WriteLine(converter(10));

このように、ラムダ式とFuncを使うと、短い処理を簡潔に書けます。

10-5. eventを使った簡単な通知処理を作る

最後に、eventを使った簡単な通知処理を作ってみましょう。

C#
class Notifier
{
public event EventHandler? Notified;

public void Send()
{
Console.WriteLine("通知を送信します");
Notified?.Invoke(this, EventArgs.Empty);
}
}

Notifier notifier = new Notifier();

notifier.Notified += (sender, e) =>
{
Console.WriteLine("通知を受け取りました");
};

notifier.Send();

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

通知を送信します
通知を受け取りました

独自のメッセージを渡したい場合は、EventArgsを継承したクラスを作ります。

C#
class NotifyEventArgs : EventArgs
{
public string Message { get; }

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

class Notifier
{
public event EventHandler<NotifyEventArgs>? Notified;

public void Send(string message)
{
Notified?.Invoke(this, new NotifyEventArgs(message));
}
}

Notifier notifier = new Notifier();

notifier.Notified += (sender, e) =>
{
Console.WriteLine(e.Message);
};

notifier.Send("新しい通知があります");

この練習を通して、delegate、event、ラムダ式の関係が理解しやすくなります。

まとめ

C#のdelegateは、メソッドを変数のように扱うための仕組みです。
最初は難しく感じるかもしれませんが、考え方は「処理そのものを渡せる型」と理解するとわかりやすくなります。

delegateを使うと、メソッドを変数に代入したり、引数として渡したり、あとから呼び出したりできます。
これにより、コールバック処理、イベント処理、処理の切り替え、共通化などを柔軟に実装できます。

基本的なdelegateの書き方は次のとおりです。

C#
delegate void MyDelegate();

戻り値や引数がある場合は、メソッドのシグネチャに合わせて定義します。

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

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

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

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

戻り値がない処理ならAction、戻り値がある処理ならFuncを使うと覚えておくとよいでしょう。

ラムダ式を使うと、delegateに渡す処理を短く書けます。

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

さらに、delegateはeventの基礎にもなっています。
eventはdelegateを安全に扱うための仕組みで、外部から勝手に呼び出されたり上書きされたりしないように制限できます。

C# delegateを理解すると、ラムダ式、Action、Func、Predicate、LINQ、event、非同期処理など、多くのC#の機能が理解しやすくなります。

初心者のうちは、まず次の流れで学ぶのがおすすめです。

  1. delegateはメソッドを変数のように扱う仕組みだと理解する

  2. delegate型を宣言して、メソッドを代入して呼び出してみる

  3. ActionとFuncで同じことを書いてみる

  4. ラムダ式で短く書いてみる

  5. eventとの違いを理解する

csharp delegateは、C#の中でも重要な基礎知識です。
一度理解できると、C#らしい柔軟なコードが書けるようになります。