C# delegate Invokeとは?使い方・null安全な呼び出し・Action/Funcとの違いを初心者向けに解説
はじめに
C#を学び始めると、delegateやInvokeという言葉が出てきて「結局何をしているの?」と混乱しやすいものです。
特に、次のようなコードを見たときに戸惑う方は多いでしょう。
C#callback.Invoke();
または、イベント処理でよく見る次の書き方もあります。
C#Completed?.Invoke();
Invokeは、簡単に言うと「デリゲートに登録されているメソッドを呼び出すための仕組み」です。
ただしC#では、Invokeを明示しなくてもデリゲートを呼び出せます。そのため、初心者にとっては「Invokeを書く場合と書かない場合で何が違うのか」「?.Invokeはなぜよく使われるのか」「ActionやFuncとは何が違うのか」が分かりにくくなりがちです。
この記事では、c# delegate invokeについて、初心者にも分かるように基本から実践例、よくあるエラー、設計上の注意点まで順番に解説します。
1. C#のdelegate Invokeとは?まず結論から理解する
1-1. delegate Invokeは「デリゲートに登録されたメソッドを呼び出す仕組み」
C#のdelegate Invokeとは、デリゲート変数に代入されているメソッドを実行するための呼び出しです。
たとえば、次のようなデリゲートがあるとします。
C#delegate void MessageHandler();
static void ShowMessage()
{
Console.WriteLine("メッセージを表示します");
}
このメソッドをデリゲートに代入して、Invokeで呼び出すことができます。
C#MessageHandler handler = ShowMessage;
handler.Invoke();
実行結果は次のようになります。
C#メッセージを表示します
つまり、handler.Invoke()は「handlerに登録されているメソッドを実行して」という意味です。
デリゲートはメソッドそのものを変数のように扱える仕組みであり、Invokeはその変数に入っているメソッドを呼び出すためのメソッドです。
1-2. Invokeを使う場合と、delegate変数をそのまま呼び出す場合の違い
C#では、デリゲートを呼び出すときにInvokeを明示しなくても同じように実行できます。
C#handler.Invoke();
これは、次の書き方とほぼ同じ意味です。
C#handler();
どちらも、handlerに登録されたShowMessageメソッドを呼び出します。
そのため、基本的には次のように理解すれば大丈夫です。
C#handler.Invoke();
handler();
この2つは、通常のデリゲート呼び出しでは同じように使えます。
ただし、Invokeを明示すると「これはデリゲートを呼び出しているのだ」と意図が分かりやすくなる場合があります。一方で、handler()のほうが短く書けるため、シンプルなコードではこちらが使われることも多いです。
1-3. 初心者が「Invoke」で混乱しやすいポイント
Invokeで混乱しやすい理由は、主に次の3つです。
まず、デリゲート自体が「メソッドを変数に入れる」という少し特殊な考え方だからです。通常の変数には数値や文字列を入れますが、デリゲートにはメソッドを代入できます。この考え方に慣れていないと、Invokeの役割も分かりにくくなります。
次に、Invokeを書いても書かなくても呼び出せるためです。
C#handler.Invoke();
handler();
どちらも同じように動くので、「ではInvokeは不要なのでは?」と感じるかもしれません。
さらに、イベント処理では次のような書き方がよく出てきます。
C#SomeEvent?.Invoke(this, EventArgs.Empty);
この?.Invokeはnull安全な呼び出しで、通常のInvokeより少し意味が増えます。ここを理解すると、イベントやコールバックのコードが読みやすくなります。
1-4. この記事でわかること
この記事では、次の内容を順番に解説します。
delegateの基本、Invokeの使い方、?.Invokeによるnull安全な呼び出し、ActionやFuncとの違い、イベント処理との関係、よくあるエラーと対処法まで扱います。
読み終えるころには、次のようなコードの意味が自然に分かるようになります。
C#callback?.Invoke("完了しました");
そして、自分のコードでも安全にデリゲートを呼び出せるようになります。
2. C#のdelegateの基本をおさらい
2-1. delegateとは何か
C#のdelegateとは、メソッドを参照するための型です。
もう少し簡単に言うと、「メソッドを変数のように扱うための仕組み」です。
通常、変数には数値や文字列を代入します。
C#int number = 10;
string message = "Hello";
一方、デリゲートにはメソッドを代入できます。
C#MessageHandler handler = ShowMessage;
これにより、あとからそのメソッドを呼び出したり、別のメソッドに差し替えたり、引数として渡したりできます。
2-2. delegateでメソッドを変数のように扱える理由
デリゲートは、呼び出せるメソッドの形を定義します。
たとえば、次のデリゲートを考えます。
C#delegate void MessageHandler();
これは「引数を受け取らず、戻り値も返さないメソッドを参照できる型」です。
そのため、次のようなメソッドを代入できます。
C#static void ShowMessage()
{
Console.WriteLine("Hello");
}
なぜなら、ShowMessageは「引数なし」「戻り値なし」という形に一致しているからです。
一方、次のようなメソッドは代入できません。
C#static int GetNumber()
{
return 10;
}
このメソッドは戻り値がintなので、delegate void MessageHandler()とは形が一致しません。
デリゲートでは、引数の型や数、戻り値の型が重要です。これらが一致しているメソッドだけを代入できます。
2-3. delegateの基本構文
デリゲートの基本構文は次のとおりです。
C#delegate 戻り値の型 デリゲート名(引数);
たとえば、引数なし・戻り値なしのデリゲートは次のように書きます。
C#delegate void MyDelegate();
文字列を受け取って戻り値なしのデリゲートは、次のように書きます。
C#delegate void MessageDelegate(string message);
2つの整数を受け取って、整数を返すデリゲートは次のように書きます。
C#delegate int CalculateDelegate(int x, int y);
このように、デリゲートは「どのような形のメソッドを代入できるか」を定義します。
2-4. delegateにメソッドを代入して呼び出す流れ
デリゲートを使う基本的な流れは、次の3ステップです。
まず、デリゲート型を定義します。
C#delegate void MessageHandler();
次に、デリゲートに代入できる形のメソッドを用意します。
C#static void ShowMessage()
{
Console.WriteLine("こんにちは");
}
最後に、デリゲート変数にメソッドを代入して呼び出します。
C#MessageHandler handler = ShowMessage;
handler.Invoke();
全体のコードは次のようになります。
C#using System;
class Program
{
delegate void MessageHandler();
static void Main()
{
MessageHandler handler = ShowMessage;
handler.Invoke();
}
static void ShowMessage()
{
Console.WriteLine("こんにちは");
}
}
このコードを実行すると、次のように表示されます。
C#こんにちは
2-5. delegateが使われる代表的な場面
デリゲートは、主に「処理をあとから渡したい」ときに使われます。
代表的な場面としては、コールバック処理があります。たとえば、ある処理が終わったあとに実行する処理を外から渡す場合です。
また、条件に応じて処理を差し替えたいときにも使われます。たとえば、計算方法や並び替えのルールを状況によって変えたい場合です。
さらに、イベント処理にもデリゲートは深く関係しています。ボタンがクリックされたとき、処理が完了したとき、値が変更されたときなど、「何かが起きたときに登録済みのメソッドを呼び出す」仕組みでデリゲートが使われます。
C#では、ActionやFunc、イベント、ラムダ式などもデリゲートと関係しています。そのため、delegate Invokeを理解しておくと、C#のさまざまなコードが読みやすくなります。
3. delegate Invokeの基本的な使い方
3-1. Invokeメソッドの基本構文
デリゲートのInvokeは、次のように使います。
C#デリゲート変数.Invoke(引数);
引数がない場合は、次のようになります。
C#handler.Invoke();
引数がある場合は、通常のメソッド呼び出しと同じように値を渡します。
C#handler.Invoke("こんにちは");
戻り値がある場合は、戻り値を変数で受け取れます。
C#int result = calculator.Invoke(10, 20);
Invokeの引数や戻り値は、デリゲート定義によって決まります。
3-2. 引数なし・戻り値なしのdelegate Invoke
まずは、もっともシンプルな例です。
C#using System;
class Program
{
delegate void Notify();
static void Main()
{
Notify notify = ShowStart;
notify.Invoke();
}
static void ShowStart()
{
Console.WriteLine("処理を開始します");
}
}
この例では、Notifyというデリゲートを定義しています。
C#delegate void Notify();
これは「引数なし、戻り値なしのメソッドを代入できる」という意味です。
ShowStartメソッドも引数なし、戻り値なしなので、Notify型の変数に代入できます。
C#Notify notify = ShowStart;
そして、次のコードで呼び出します。
C#notify.Invoke();
実行結果は次のとおりです。
C#処理を開始します
3-3. 引数ありのdelegate Invoke
次に、引数ありのデリゲートを見てみましょう。
C#using System;
class Program
{
delegate void MessageHandler(string message);
static void Main()
{
MessageHandler handler = ShowMessage;
handler.Invoke("こんにちは");
}
static void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
このデリゲートは、string型の引数を1つ受け取るメソッドを代入できます。
C#delegate void MessageHandler(string message);
そのため、次のメソッドを代入できます。
C#static void ShowMessage(string message)
{
Console.WriteLine(message);
}
呼び出すときは、Invokeの引数に文字列を渡します。
C#handler.Invoke("こんにちは");
実行結果は次のとおりです。
C#こんにちは
通常のメソッド呼び出しと同じように、デリゲートの引数にも型の一致が必要です。
3-4. 戻り値ありのdelegate Invoke
戻り値があるデリゲートも使えます。
C#using System;
class Program
{
delegate int Calculator(int x, int y);
static void Main()
{
Calculator calculator = Add;
int result = calculator.Invoke(10, 20);
Console.WriteLine(result);
}
static int Add(int x, int y)
{
return x + y;
}
}
この例では、Calculatorというデリゲートを定義しています。
C#delegate int Calculator(int x, int y);
これは「int型の引数を2つ受け取り、int型の値を返すメソッド」を代入できる型です。
Addメソッドはこの条件に一致しています。
C#static int Add(int x, int y)
{
return x + y;
}
そのため、次のように代入できます。
C#Calculator calculator = Add;
そして、Invokeの戻り値をresultに代入します。
C#int result = calculator.Invoke(10, 20);
実行結果は次のとおりです。
C#30
3-5. Invokeを省略して呼び出す書き方
デリゲートは、Invokeを省略して呼び出すこともできます。
C#calculator.Invoke(10, 20);
これは、次のように書けます。
C#calculator(10, 20);
全体のコードで見ると、次のようになります。
C#using System;
class Program
{
delegate int Calculator(int x, int y);
static void Main()
{
Calculator calculator = Add;
int result = calculator(10, 20);
Console.WriteLine(result);
}
static int Add(int x, int y)
{
return x + y;
}
}
実行結果は同じです。
C#30
C#では、この省略形がよく使われます。理由は、通常のメソッド呼び出しのように自然に読めるからです。
3-6. Invokeを明示したほうが読みやすいケース
Invokeは省略できますが、明示したほうが読みやすいケースもあります。
たとえば、コールバックであることを強調したい場合です。
C#onCompleted.Invoke();
このコードを見ると、「onCompletedに登録された処理を呼び出している」と分かりやすくなります。
また、null条件演算子と組み合わせる場合は、Invokeを明示します。
C#onCompleted?.Invoke();
この書き方は、デリゲートがnullでなければ呼び出し、nullなら何もしないという意味です。
イベント処理では、この?.Invokeが非常によく使われます。
C#Completed?.Invoke(this, EventArgs.Empty);
そのため、実務では「通常の呼び出しではhandler()、null安全に呼び出すときはhandler?.Invoke()」という使い分けをよく見かけます。
4. delegate Invokeをnull安全に呼び出す方法
4-1. delegateがnullのときに起きるNullReferenceException
デリゲート変数にメソッドが代入されていない場合、その値はnullです。
C#MessageHandler handler = null;
この状態でInvokeを呼び出すと、NullReferenceExceptionが発生します。
C#handler.Invoke();
また、Invokeを省略しても同じです。
C#handler();
どちらも、handlerがnullなので呼び出せません。
たとえば、次のコードは例外になります。
C#using System;
class Program
{
delegate void MessageHandler();
static void Main()
{
MessageHandler handler = null;
handler.Invoke();
}
}
handlerには何もメソッドが登録されていないため、呼び出し先が存在しません。その結果、NullReferenceExceptionが発生します。
4-2. if文でnullチェックしてからInvokeする方法
基本的な対策は、if文でnullチェックをしてから呼び出す方法です。
C#if (handler != null)
{
handler.Invoke();
}
全体の例は次のとおりです。
C#using System;
class Program
{
delegate void MessageHandler();
static void Main()
{
MessageHandler handler = null;
if (handler != null)
{
handler.Invoke();
}
Console.WriteLine("終了しました");
}
}
この場合、handlerがnullなのでInvokeは実行されません。例外も発生せず、次のように表示されます。
C#終了しました
もちろん、メソッドが代入されていれば呼び出されます。
C#MessageHandler handler = ShowMessage;
if (handler != null)
{
handler.Invoke();
}
この書き方は分かりやすいですが、少し冗長です。
4-3. null条件演算子?.Invokeを使う方法
C#では、null安全にデリゲートを呼び出すために?.Invokeがよく使われます。
C#handler?.Invoke();
これは、次の意味です。
C#if (handler != null)
{
handler.Invoke();
}
引数がある場合も同じように書けます。
C#handler?.Invoke("こんにちは");
戻り値がある場合は、戻り値がnull許容になる点に注意が必要です。
C#delegate int Calculator(int x, int y);
Calculator calculator = null;
int? result = calculator?.Invoke(10, 20);
calculatorがnullの場合、calculator?.Invoke(10, 20)の結果はnullになります。そのため、戻り値はintではなくint?として扱う必要があります。
デフォルト値を使いたい場合は、??演算子と組み合わせると便利です。
C#int result = calculator?.Invoke(10, 20) ?? 0;
この場合、calculatorがnullなら0が入ります。
4-4. ?.Invokeが推奨される理由
?.Invokeがよく使われる理由は、短く安全に書けるからです。
C#handler?.Invoke();
この1行で、「handlerがnullでなければ呼び出す」という処理を表現できます。
イベント発火でも、次のように使われます。
C#Completed?.Invoke(this, EventArgs.Empty);
この書き方なら、イベントに誰も登録していない場合でも例外になりません。
また、if文よりもコードが簡潔になり、イベント発火の定番パターンとして読みやすくなります。
ただし、戻り値があるデリゲートでは、?.Invokeの結果がnull許容になることを忘れないようにしましょう。
4-5. マルチスレッド時に注意したいnullチェックの落とし穴
次のようなコードは、一見安全に見えます。
C#if (handler != null)
{
handler.Invoke();
}
しかし、マルチスレッド環境では注意が必要です。
if (handler != null)でチェックした直後に、別のスレッドがhandlerをnullにする可能性があるからです。その場合、handler.Invoke()の時点でnullになっていて、NullReferenceExceptionが発生することがあります。
古い書き方では、ローカル変数にコピーしてから呼び出す方法がよく使われていました。
C#var temp = handler;
if (temp != null)
{
temp.Invoke();
}
このようにすると、チェックした対象と呼び出す対象が同じローカル変数になるため、安全性が高まります。
現在では、イベントやデリゲートの呼び出しで次の書き方がよく使われます。
C#handler?.Invoke();
?.Invokeは簡潔で、一般的なnull安全な呼び出しとして広く使われています。
4-6. null安全なdelegate呼び出しのベストプラクティス
デリゲートを呼び出すときは、基本的にnullの可能性を考えることが大切です。
メソッドが必ず代入されていると分かっている場合は、次のように呼び出しても問題ありません。
C#handler();
または、次のように書けます。
C#handler.Invoke();
しかし、コールバックやイベントのように「登録されているか分からない」場合は、?.Invokeを使うのが安全です。
C#handler?.Invoke();
特にイベント発火では、次のような書き方が定番です。
C#SomeEvent?.Invoke(this, EventArgs.Empty);
初心者のうちは、デリゲートがnullになり得る場面では?.Invokeを使う、と覚えておくとよいでしょう。
5. delegate InvokeとAction・Funcの違い
5-1. Actionとは何か
Actionは、戻り値がないデリゲートを簡単に表すための組み込みデリゲートです。
たとえば、次の自作デリゲートがあるとします。
C#delegate void MessageHandler(string message);
これは、Action<string>で置き換えることができます。
C#Action<string> handler;
Action<string>は、「stringを1つ受け取り、戻り値がないメソッド」を表します。
たとえば、次のように使えます。
C#using System;
class Program
{
static void Main()
{
Action<string> action = ShowMessage;
action.Invoke("こんにちは");
}
static void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
Actionは戻り値がない処理に使います。
引数なしの場合は、次のように書きます。
C#Action action = ShowMessage;
複数の引数がある場合は、次のように書きます。
C#Action<string, int> action = ShowUser;
5-2. Funcとは何か
Funcは、戻り値があるデリゲートを簡単に表すための組み込みデリゲートです。
たとえば、次の自作デリゲートがあるとします。
C#delegate int Calculator(int x, int y);
これは、Func<int, int, int>で置き換えることができます。
C#Func<int, int, int> calculator;
Func<int, int, int>の最後のintが戻り値の型です。
つまり、次の意味になります。
C#Func<第1引数の型, 第2引数の型, 戻り値の型>
実際のコードは次のとおりです。
C#using System;
class Program
{
static void Main()
{
Func<int, int, int> calculator = Add;
int result = calculator.Invoke(10, 20);
Console.WriteLine(result);
}
static int Add(int x, int y)
{
return x + y;
}
}
実行結果は次のようになります。
C#30
Funcは、戻り値がある処理を表すときに使います。
5-3. delegateとAction/Funcの関係
ActionやFuncも、実はデリゲートです。
つまり、自作のdelegateと同じように、メソッドを代入して呼び出せます。
自作デリゲートを使う場合は、次のように書きます。
C#delegate int Calculator(int x, int y);
Calculator calculator = Add;
Funcを使う場合は、次のように書きます。
C#Func<int, int, int> calculator = Add;
どちらも、Addメソッドを代入して呼び出せます。
C#int result = calculator.Invoke(10, 20);
違いは、専用の名前を持つデリゲート型を自分で作るか、既存の汎用デリゲート型を使うかです。
5-4. Invokeの使い方はAction/Funcでも同じ
ActionやFuncでも、Invokeの使い方は同じです。
Actionの場合は次のように呼び出せます。
C#Action<string> action = message => Console.WriteLine(message);
action.Invoke("Hello");
また、Invokeを省略することもできます。
C#action("Hello");
Funcの場合も同じです。
C#Func<int, int, int> add = (x, y) => x + y;
int result1 = add.Invoke(10, 20);
int result2 = add(10, 20);
どちらの書き方でも結果は同じです。
C#30
null安全に呼び出す場合は、ActionやFuncでも?.Invokeが使えます。
C#Action<string> action = null;
action?.Invoke("Hello");
Funcの場合は戻り値がnull許容になる点に注意します。
C#Func<int, int, int> add = null;
int result = add?.Invoke(10, 20) ?? 0;
5-5. 自作delegateを使うべきケース
自作delegateを使うべきなのは、その処理に意味のある名前を付けたい場合です。
たとえば、次のようなデリゲート名は、コードの意図を分かりやすくします。
C#delegate void OrderCompletedHandler(Order order);
この名前を見ると、「注文完了時に呼び出される処理」だと分かります。
一方、Action<Order>だけでは、何のための処理なのかは変数名や文脈を読まないと分かりません。
C#Action<Order> handler;
イベントやライブラリ設計、公開APIなどでは、意味のあるデリゲート型を定義したほうが読みやすくなる場合があります。
また、特定の用途で何度も同じシグネチャを使う場合にも、自作デリゲートは便利です。
5-6. Action/Funcを使うべきケース
ActionやFuncを使うべきなのは、シンプルな処理を手軽に渡したい場合です。
たとえば、次のようなケースです。
C#void Execute(Action action)
{
action?.Invoke();
}
また、簡単な計算処理を渡したい場合もFuncが便利です。
C#int Calculate(int x, int y, Func<int, int, int> operation)
{
return operation.Invoke(x, y);
}
このように、短いコールバックや内部処理では、ActionやFuncを使うとコードが簡潔になります。
特にラムダ式と組み合わせる場合は、ActionやFuncがよく使われます。
C#Action action = () => Console.WriteLine("実行しました");
Func<int, int, int> add = (x, y) => x + y;
5-7. 初心者向けの使い分け早見表
delegate、Action、Funcの使い分けは、次のように考えると分かりやすいです。
| 種類 | 戻り値 | 主な用途 | 例 |
|---|---|---|---|
| 自作delegate | あり・なし両方可能 | 意味のある型名を付けたいとき | delegate void CompletedHandler(); |
| Action | なし | 戻り値なしの処理を簡単に渡したいとき | Action<string> |
| Func | あり | 戻り値ありの処理を簡単に渡したいとき | Func<int, int, int> |
初心者のうちは、次のように覚えるとよいでしょう。
戻り値がないならAction、戻り値があるならFunc、処理に名前を付けて意味を明確にしたいなら自作delegateです。
6. delegate Invokeの実践例
6-1. コールバック処理で使う例
コールバックとは、「ある処理が終わったあとに呼び出される処理」のことです。
たとえば、データを保存したあとに完了メッセージを表示したい場合を考えます。
C#using System;
class Program
{
delegate void CompletedHandler();
static void Main()
{
SaveData(OnCompleted);
}
static void SaveData(CompletedHandler onCompleted)
{
Console.WriteLine("データを保存しています...");
onCompleted?.Invoke();
}
static void OnCompleted()
{
Console.WriteLine("保存が完了しました");
}
}
実行結果は次のようになります。
C#データを保存しています...
保存が完了しました
SaveDataメソッドは、保存後に実行する処理をonCompletedとして受け取っています。
C#static void SaveData(CompletedHandler onCompleted)
そして、保存処理の最後で呼び出しています。
C#onCompleted?.Invoke();
このようにすると、「保存処理」と「保存後に何をするか」を分けて書けます。
6-2. 条件に応じて処理を差し替える例
デリゲートを使うと、条件に応じて実行する処理を差し替えられます。
C#using System;
class Program
{
delegate int Calculator(int x, int y);
static void Main()
{
string mode = "add";
Calculator calculator;
if (mode == "add")
{
calculator = Add;
}
else
{
calculator = Subtract;
}
int result = calculator.Invoke(10, 5);
Console.WriteLine(result);
}
static int Add(int x, int y)
{
return x + y;
}
static int Subtract(int x, int y)
{
return x - y;
}
}
modeが"add"なら足し算、そうでなければ引き算を実行します。
このように、デリゲートを使うと「どの処理を実行するか」を変数として扱えます。
Funcを使うと、さらに短く書けます。
C#Func<int, int, int> calculator = mode == "add"
? Add
: Subtract;
int result = calculator.Invoke(10, 5);
6-3. 複数のメソッドをdelegateに登録してInvokeする例
デリゲートには、複数のメソッドを登録できます。これをマルチキャストデリゲートと呼びます。
C#using System;
class Program
{
delegate void Notify();
static void Main()
{
Notify notify = ShowMessage1;
notify += ShowMessage2;
notify += ShowMessage3;
notify.Invoke();
}
static void ShowMessage1()
{
Console.WriteLine("メッセージ1");
}
static void ShowMessage2()
{
Console.WriteLine("メッセージ2");
}
static void ShowMessage3()
{
Console.WriteLine("メッセージ3");
}
}
実行結果は次のとおりです。
C#メッセージ1
メッセージ2
メッセージ3
+=を使うと、デリゲートにメソッドを追加できます。
C#notify += ShowMessage2;
Invokeすると、登録されたメソッドが順番に実行されます。
なお、登録を解除する場合は-=を使います。
C#notify -= ShowMessage2;
6-4. イベント処理で?.Invokeを使う例
イベント処理では、?.Invokeがよく使われます。
C#using System;
class Downloader
{
public event Action Completed;
public void Download()
{
Console.WriteLine("ダウンロード中...");
Completed?.Invoke();
}
}
class Program
{
static void Main()
{
var downloader = new Downloader();
downloader.Completed += () =>
{
Console.WriteLine("ダウンロードが完了しました");
};
downloader.Download();
}
}
実行結果は次のようになります。
C#ダウンロード中...
ダウンロードが完了しました
Completed?.Invoke()は、Completedイベントに処理が登録されていれば呼び出し、登録されていなければ何もしません。
イベントでは、常に誰かが購読しているとは限りません。そのため、null安全に呼び出す?.Invokeがよく使われます。
6-5. ラムダ式とdelegate Invokeを組み合わせる例
デリゲートには、通常のメソッドだけでなくラムダ式も代入できます。
C#using System;
class Program
{
delegate void MessageHandler(string message);
static void Main()
{
MessageHandler handler = message =>
{
Console.WriteLine($"受け取ったメッセージ: {message}");
};
handler.Invoke("こんにちは");
}
}
実行結果は次のとおりです。
C#受け取ったメッセージ: こんにちは
Actionを使うと、より簡潔に書けます。
C#Action<string> handler = message =>
{
Console.WriteLine($"受け取ったメッセージ: {message}");
};
handler?.Invoke("こんにちは");
ラムダ式とInvokeを組み合わせると、その場で処理を定義して呼び出せます。コールバックや一時的な処理の差し替えでよく使われます。
7. delegate Invokeとイベントの関係
7-1. eventとdelegateの違い
C#のeventは、デリゲートをもとにした仕組みです。
デリゲートはメソッドを登録して呼び出せます。
C#Action action;
action = () => Console.WriteLine("実行");
action.Invoke();
一方、イベントは「外部からは登録と解除だけできるように制限したデリゲート」と考えると分かりやすいです。
C#public event Action Completed;
イベントに対して、外部のクラスは+=で処理を登録できます。
C#downloader.Completed += OnCompleted;
また、-=で解除できます。
C#downloader.Completed -= OnCompleted;
しかし、外部から直接Invokeすることはできません。
C#downloader.Completed.Invoke(); // 外部からはできない
イベントは、発火する責任をイベントを持つクラス自身に限定するための仕組みです。
7-2. イベント発火でInvokeが使われる理由
イベントは、登録されたイベントハンドラーを呼び出すためにInvokeを使います。
たとえば、次のようなイベントがあるとします。
C#public event Action Completed;
このイベントを発火するには、クラス内部で次のように書きます。
C#Completed?.Invoke();
これは、「Completedに登録されているすべての処理を呼び出す」という意味です。
イベントに複数の処理が登録されていれば、順番に呼び出されます。
C#downloader.Completed += Handler1;
downloader.Completed += Handler2;
この状態でCompleted?.Invoke()を実行すると、Handler1とHandler2が呼び出されます。
7-3. On〇〇メソッドで?.Invokeする定番パターン
C#では、イベントを発火するためにOn〇〇というメソッドを用意するパターンがよく使われます。
C#using System;
class Worker
{
public event EventHandler Completed;
public void Run()
{
Console.WriteLine("処理中...");
OnCompleted();
}
protected virtual void OnCompleted()
{
Completed?.Invoke(this, EventArgs.Empty);
}
}
この例では、Runメソッドの最後でOnCompletedを呼び出しています。
C#OnCompleted();
そして、OnCompletedの中でイベントを発火しています。
C#Completed?.Invoke(this, EventArgs.Empty);
EventHandlerは、C#のイベントでよく使われるデリゲート型です。第1引数にはイベントの発生元、第2引数にはイベントデータを渡します。
C#Completed?.Invoke(this, EventArgs.Empty);
イベントデータが特にない場合は、EventArgs.Emptyを渡します。
7-4. eventでは外部からInvokeできない理由
eventでは、外部からInvokeできません。
たとえば、次のようなクラスがあるとします。
C#class Worker
{
public event Action Completed;
public void Run()
{
Completed?.Invoke();
}
}
外部からイベントに処理を登録することはできます。
C#var worker = new Worker();
worker.Completed += () =>
{
Console.WriteLine("完了しました");
};
しかし、外部から次のように呼び出すことはできません。
C#worker.Completed?.Invoke(); // コンパイルエラー
これは、イベントの発火をクラスの外部に許可してしまうと、クラスの状態と関係なく勝手にイベントが発生したことにできてしまうからです。
イベントは、「その出来事が本当に起きたときに、所有しているクラスが発火する」べきものです。そのため、外部からは登録と解除だけができ、呼び出しは内部に制限されています。
7-5. イベント実装でよくあるミス
イベント実装でよくあるミスの1つは、nullチェックをせずにInvokeしてしまうことです。
C#Completed.Invoke();
イベントに誰も登録していない場合、Completedはnullなので例外になります。
安全に書くなら、次のようにします。
C#Completed?.Invoke();
また、イベントを外部から発火しようとするのもよくあるミスです。
C#worker.Completed?.Invoke();
eventは外部から呼び出せないため、このようなコードはコンパイルエラーになります。
さらに、イベントハンドラーの引数を間違えるケースもあります。
C#public event EventHandler Completed;
Completed?.Invoke(); // 引数が足りない
EventHandlerは、object senderとEventArgs eの2つの引数が必要です。
正しくは次のように書きます。
C#Completed?.Invoke(this, EventArgs.Empty);
8. delegate Invokeでよくあるエラーと対処法
8-1. NullReferenceExceptionが発生する
もっともよくあるエラーは、デリゲートがnullの状態でInvokeしてしまうことです。
C#Action action = null;
action.Invoke();
このコードはNullReferenceExceptionになります。
対処法は、?.Invokeを使うことです。
C#action?.Invoke();
または、if文でnullチェックします。
C#if (action != null)
{
action.Invoke();
}
イベントやコールバックのように、登録されているか分からない処理では、?.Invokeを使うのが安全です。
8-2. 引数の型や数が一致しない
デリゲートに代入するメソッドは、引数の型や数が一致している必要があります。
たとえば、次のデリゲートを考えます。
C#delegate void MessageHandler(string message);
このデリゲートには、stringを1つ受け取るメソッドを代入できます。
C#static void ShowMessage(string message)
{
Console.WriteLine(message);
}
しかし、次のメソッドは代入できません。
C#static void ShowNumber(int number)
{
Console.WriteLine(number);
}
引数の型がstringではなくintだからです。
また、引数の数が違う場合も代入できません。
C#static void ShowMessage(string message, int count)
{
Console.WriteLine(message);
}
呼び出すときも、デリゲートの定義に合った引数を渡す必要があります。
C#handler.Invoke("こんにちは");
次のように引数が足りないとコンパイルエラーになります。
C#handler.Invoke();
8-3. 戻り値の扱いを間違える
戻り値があるデリゲートでは、戻り値の型にも注意が必要です。
C#Func<int, int, int> add = (x, y) => x + y;
int result = add.Invoke(10, 20);
これは正しいコードです。
しかし、戻り値があるデリゲートをActionのように扱うと、意図した値を受け取れません。
C#add.Invoke(10, 20);
この書き方でも呼び出し自体はできますが、戻り値を使っていません。計算結果が必要なら、変数に代入しましょう。
C#int result = add.Invoke(10, 20);
また、?.Invokeを使う場合は戻り値がnull許容になることがあります。
C#Func<int, int, int> add = null;
int? result = add?.Invoke(10, 20);
通常のintとして扱いたい場合は、デフォルト値を指定します。
C#int result = add?.Invoke(10, 20) ?? 0;
8-4. 複数登録したdelegateの戻り値で混乱する
デリゲートには複数のメソッドを登録できます。
C#Func<int> func = GetOne;
func += GetTwo;
func += GetThree;
この状態でInvokeすると、登録されたメソッドは順番に実行されます。
ただし、戻り値として受け取れるのは最後に実行されたメソッドの戻り値です。
C#int result = func.Invoke();
次の例を見てみましょう。
C#using System;
class Program
{
static void Main()
{
Func<int> func = GetOne;
func += GetTwo;
func += GetThree;
int result = func.Invoke();
Console.WriteLine(result);
}
static int GetOne()
{
Console.WriteLine("GetOne");
return 1;
}
static int GetTwo()
{
Console.WriteLine("GetTwo");
return 2;
}
static int GetThree()
{
Console.WriteLine("GetThree");
return 3;
}
}
実行結果は次のようになります。
C#GetOne
GetTwo
GetThree
3
GetOne、GetTwo、GetThreeはすべて実行されますが、resultに入るのは最後のGetThreeの戻り値です。
複数の戻り値をすべて扱いたい場合は、デリゲートを単純にInvokeするのではなく、呼び出しリストを取得して個別に実行する方法があります。
C#foreach (Func<int> item in func.GetInvocationList())
{
int value = item.Invoke();
Console.WriteLine($"戻り値: {value}");
}
戻り値のあるマルチキャストデリゲートは混乱しやすいため、設計時には注意が必要です。
8-5. async処理とdelegate Invokeを組み合わせるときの注意点
非同期処理では、ActionではなくFunc<Task>を使う場面があります。
たとえば、非同期のコールバックを受け取る場合は次のように書けます。
C#using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Func<Task> callback = async () =>
{
await Task.Delay(1000);
Console.WriteLine("非同期処理が完了しました");
};
await callback.Invoke();
}
}
Func<Task>は、「戻り値としてTaskを返す処理」を表します。
注意したいのは、Actionにasyncラムダを代入すると、async voidのような扱いになり、例外処理や完了待ちが難しくなることです。
C#Action action = async () =>
{
await Task.Delay(1000);
Console.WriteLine("完了");
};
このような書き方は避け、非同期処理を待ちたい場合はFunc<Task>を使うのが基本です。
null安全に呼び出す場合は、次のようにします。
C#if (callback != null)
{
await callback.Invoke();
}
?.Invokeを使う場合は、戻り値がTask?になるため、次のように書けます。
C#await (callback?.Invoke() ?? Task.CompletedTask);
これにより、callbackがnullでも安全に処理できます。
8-6. InvokeとBeginInvokeを混同しない
古いC#のコードでは、BeginInvokeというメソッドを見かけることがあります。
C#handler.BeginInvoke(null, null);
Invokeは同期的にメソッドを呼び出します。つまり、呼び出した処理が終わるまで次の処理に進みません。
一方、BeginInvokeは古い非同期呼び出しの仕組みで使われていました。
現在のC#では、非同期処理にはasyncとawait、Taskを使うのが一般的です。
そのため、初心者のうちは次のように覚えておくとよいでしょう。
通常のデリゲート呼び出しではInvokeを使います。非同期処理をしたい場合は、BeginInvokeではなく、Func<Task>やasync/awaitを検討します。
9. delegate Invokeを使うときの設計上の注意点
9-1. 読みやすさを優先する
Invokeを使うか省略するかは、読みやすさを基準に考えるとよいです。
シンプルなデリゲート呼び出しでは、次のように省略しても自然です。
C#handler();
一方、コールバックやイベント発火であることを明確にしたい場合は、Invokeを明示すると分かりやすくなります。
C#onCompleted?.Invoke();
特に?.Invokeは定番の書き方なので、イベントやコールバックでは積極的に使って問題ありません。
大切なのは、チームやプロジェクト内で読みやすい書き方に統一することです。
9-2. null安全な呼び出しを徹底する
デリゲートは、何も代入されていなければnullです。
そのため、呼び出し前にはnullの可能性を考える必要があります。
安全な書き方は次のとおりです。
C#handler?.Invoke();
引数がある場合も同じです。
C#handler?.Invoke("完了しました");
イベントの場合も、次のように書きます。
C#Completed?.Invoke(this, EventArgs.Empty);
デリゲートが必ず代入されていると保証できる場面以外では、?.Invokeを使うのが安全です。
9-3. delegateを多用しすぎない
デリゲートは便利ですが、多用しすぎると処理の流れが追いにくくなります。
たとえば、あちこちでコールバックを渡していると、「最終的にどのメソッドが呼ばれているのか」が分かりにくくなることがあります。
C#service.Execute(onSuccess, onError, onRetry);
このようなコードが増えると、処理の見通しが悪くなる場合があります。
単純な処理ならデリゲートで問題ありませんが、処理が複雑になってきたら、クラス分割やインターフェース、イベント、DIなど別の設計を検討しましょう。
9-4. Action/Funcで十分な場面を見極める
すべての場面で自作delegateを定義する必要はありません。
短いコールバックや内部的な処理であれば、ActionやFuncで十分なことが多いです。
C#void Execute(Action action)
{
action?.Invoke();
}
戻り値がある場合はFuncを使えます。
C#int Execute(Func<int> func)
{
return func.Invoke();
}
一方、処理の意味を型名で明確にしたい場合は、自作delegateを使う価値があります。
C#delegate void OrderCompletedHandler(Order order);
判断基準は、コードを読んだ人が意図を理解しやすいかどうかです。
9-5. イベント・インターフェース・DIとの使い分け
デリゲートは「処理を渡す」ためのシンプルな仕組みです。
一方、イベントは「何かが起きたことを通知する」ために使います。
C#public event EventHandler Completed;
インターフェースは、「複数のメソッドを持つ役割」を表したいときに向いています。
C#interface ILogger
{
void Log(string message);
}
DIは、クラスの依存関係を外から渡して、テストしやすくしたり差し替えやすくしたりする設計で使われます。
単発の処理を渡すだけならデリゲートやAction、Funcで十分です。
状態変化を通知したいならイベントが向いています。
複数の関連する処理をまとめて扱いたいならインターフェースが向いています。
アプリ全体の依存関係を管理したいならDIを使うのが自然です。
delegate Invokeは便利ですが、すべてをデリゲートで解決しようとせず、用途に応じて使い分けることが大切です。
まとめ
C#のdelegate Invokeは、デリゲートに登録されたメソッドを呼び出すための仕組みです。
基本的には、次のように呼び出せます。
C#handler.Invoke();
また、Invokeは省略して書くこともできます。
C#handler();
どちらも通常は同じように動きます。
ただし、デリゲートがnullの可能性がある場合は、Invokeをそのまま呼び出すとNullReferenceExceptionが発生します。
C#handler.Invoke();
そのため、null安全に呼び出すには?.Invokeを使います。
C#handler?.Invoke();
イベント発火では、次のような書き方が定番です。
C#Completed?.Invoke(this, EventArgs.Empty);
また、ActionやFuncもデリゲートの一種なので、同じようにInvokeできます。
C#Action action = () => Console.WriteLine("Hello");
action.Invoke();
Func<int, int, int> add = (x, y) => x + y;
int result = add.Invoke(10, 20);
使い分けとしては、戻り値がない処理ならAction、戻り値がある処理ならFunc、意味のある名前を付けたい場合は自作delegateを使うと分かりやすくなります。
初心者のうちは、まず次の3つを押さえておきましょう。
Invokeはデリゲートに登録されたメソッドを呼び出すものです。
handler.Invoke()とhandler()は、通常ほぼ同じ意味です。
nullの可能性がある場合は、handler?.Invoke()を使うのが安全です。
この3点を理解しておけば、C#のコールバック、イベント、ラムダ式、Action、Funcのコードがずっと読みやすくなります。

