C#のInvokeとdelegateを完全理解|使い方・必要な場面・エラー対処まで解説
はじめに
C#でInvokeやdelegateを調べていると、同じ「Invoke」という名前でも意味が違う場面に出会います。たとえば、delegate.Invoke()は「delegateに登録されたメソッドを呼び出す」ための仕組みです。一方、WinFormsでよく使うControl.Invoke(...)は「UIスレッド上で処理を実行する」ための仕組みです。
つまり、c# invoke delegateを理解するうえで重要なのは、まず「どのInvokeの話をしているのか」を切り分けることです。この記事では、delegateの基本、delegate.Invoke()の使い方、WinFormsのControl.Invoke、BeginInvoke、InvokeAsync、よくあるエラーと対処法まで順番に解説します。
1. C#のInvokeとdelegateをまず結論から理解する
1-1. Invokeとは「delegateに登録された処理を実行する」ための呼び出し
C#のdelegateには、登録されたメソッドを実行するためのInvokeメソッドがあります。たとえば、次のようにdelegate変数にメソッドを代入し、Invoke()で呼び出せます。
C#delegate void MessageHandler(string message);
class Program
{
static void Main()
{
MessageHandler handler = ShowMessage;
handler.Invoke("こんにちは");
}
static void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
この例では、handlerというdelegate変数にShowMessageメソッドを代入しています。そしてhandler.Invoke("こんにちは")を実行すると、実際にはShowMessage("こんにちは")が呼び出されます。C#仕様では、delegate型には宣言された引数・戻り値に対応するInvokeメソッドが用意され、Invoke呼び出しと通常のdelegate呼び出し構文は意味的に同等とされています。Microsoft Learn
1-2. delegateとは「メソッドを変数のように扱う」ための型
delegateは、特定の引数リストと戻り値を持つメソッドへの参照を表す型です。簡単に言うと、「この形のメソッドなら代入できる」というルールを定義するものです。Microsoftの解説でも、delegateは特定の引数リストと戻り値を持つメソッド参照を表す型として説明されています。Microsoft Learn
C#delegate int Calculator(int x, int y);
class Program
{
static void Main()
{
Calculator calc = Add;
int result = calc.Invoke(3, 5);
Console.WriteLine(result); // 8
}
static int Add(int x, int y)
{
return x + y;
}
}
Calculatorは「intを2つ受け取り、intを返すメソッド」を参照できるdelegate型です。Addメソッドはその形に一致しているため、Calculator型の変数に代入できます。
1-3. 「delegate.Invoke」と「Control.Invoke」は意味が違う
C#初心者が混同しやすいのが、delegate.InvokeとControl.Invokeです。
delegate.Invokeは、delegateに登録されたメソッドを実行するための呼び出しです。
C#Action action = () => Console.WriteLine("実行");
action.Invoke();
一方、WinFormsのControl.Invokeは、コントロールを作成したスレッド、つまり通常はUIスレッド上でdelegateを実行するためのメソッドです。Windows Formsのコントロールは、作成元ではないスレッドから直接操作することが安全ではないため、Control.Invokeなどを使ってUIスレッドへ処理を移します。Microsoft Learn
C#this.Invoke(new Action(() =>
{
label1.Text = "更新しました";
}));
同じInvokeという名前でも、役割は次のように違います。
| 種類 | 主な意味 | よく使う場面 |
|---|---|---|
delegate.Invoke() | delegateに登録されたメソッドを実行する | コールバック、イベント、処理の差し替え |
Control.Invoke(...) | UIスレッド上でdelegateを実行する | WinFormsで別スレッドから画面を更新する |
1-4. この記事で解決できる疑問:使い方・必要な場面・エラー対処
この記事では、次の疑問を解消します。
delegateとは何かInvoke()はいつ使うのかdel()とdel.Invoke()は何が違うのか?.Invoke()はなぜ使われるのかWinFormsの
Control.Invokeは何のために必要なのかInvokeRequiredはどう使うのかInvoke、BeginInvoke、InvokeAsyncはどう使い分けるのかNullReferenceExceptionやクロススレッド例外をどう防ぐのか
C#のdelegateとInvokeは、イベント処理、非同期処理、UI更新、コールバック設計など、実務でも頻繁に出てきます。基本から順番に理解しておくと、エラー対応だけでなく、読みやすい設計にも役立ちます。
2. delegateの基本|Invokeを理解するための前提知識
2-1. delegateの役割と仕組み
delegateの役割は、「後から呼び出したい処理」を変数や引数として扱えるようにすることです。通常、メソッドは名前を書いて直接呼び出します。
C#PrintMessage("Hello");
しかしdelegateを使うと、呼び出す処理を変数に入れておき、必要なタイミングで実行できます。
C#Action<string> printer = PrintMessage;
printer.Invoke("Hello");
これにより、処理を差し替えたり、別のメソッドへ処理そのものを渡したりできます。delegateは、CやC++の関数ポインターに似ていますが、C#ではオブジェクト指向的で型安全な仕組みとして提供されています。Microsoft Learn
2-2. delegateの宣言・代入・実行の基本構文
delegateの基本は、次の3ステップです。
delegate型を宣言する
形が一致するメソッドを代入する
Invoke()または()で実行する
C#delegate void Notify(string message);
class Program
{
static void Main()
{
Notify notify = SendMessage;
notify.Invoke("処理が完了しました");
}
static void SendMessage(string message)
{
Console.WriteLine(message);
}
}
Notifyは「stringを1つ受け取り、戻り値がないメソッド」を参照できるdelegate型です。SendMessageはその条件に一致しているため、Notify型の変数に代入できます。
2-3. 引数あり・戻り値ありのdelegate
delegateは、引数だけでなく戻り値も指定できます。
C#delegate int Operation(int a, int b);
class Program
{
static void Main()
{
Operation operation = Multiply;
int result = operation.Invoke(4, 5);
Console.WriteLine(result); // 20
}
static int Multiply(int a, int b)
{
return a * b;
}
}
この場合、Operationに代入できるのは「intを2つ受け取り、intを返すメソッド」です。引数の数、型、戻り値の型が合っていないメソッドは代入できません。C#仕様でも、delegateとメソッドの互換性は、引数の数や型、戻り値などに基づいて判断されます。Microsoft Learn
2-4. Action・Func・Predicateとの違い
C#では、毎回delegateキーワードで独自のdelegate型を宣言しなくても、よく使う形は標準で用意されています。それがAction、Func、Predicateです。
| 型 | 意味 | 例 |
|---|---|---|
Action | 戻り値なしの処理 | Action<string> |
Func | 戻り値ありの処理 | Func<int, int, int> |
Predicate<T> | Tを受け取りboolを返す判定処理 | Predicate<int> |
C#Action<string> log = message => Console.WriteLine(message);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = x => x % 2 == 0;
log.Invoke("ログ出力");
Console.WriteLine(add.Invoke(2, 3)); // 5
Console.WriteLine(isEven.Invoke(10)); // True
Action<T>は戻り値なし、Func<T, TResult>は戻り値あり、Funcの最後の型引数は戻り値の型です。Microsoftの解説でも、Func<int, int, int>は2つのint入力と1つのint結果を表し、Action<string>はstring入力で戻り値なしの処理を表すと説明されています。Microsoft Learn
2-5. ラムダ式・匿名メソッドでdelegateを簡潔に書く方法
delegateには、既存のメソッドだけでなく、ラムダ式や匿名メソッドも代入できます。
C#Action<string> notify = message =>
{
Console.WriteLine($"通知: {message}");
};
notify.Invoke("登録が完了しました");
匿名メソッドで書く場合は次のようになります。
C#Action<string> notify = delegate(string message)
{
Console.WriteLine($"通知: {message}");
};
notify.Invoke("登録が完了しました");
現在のC#では、ラムダ式を使うことが一般的です。特に処理が短い場合、わざわざメソッドを別に定義しなくても、delegateに直接処理を渡せるためコードが簡潔になります。
2-6. delegateを使う代表的な場面:コールバック・イベント・処理の差し替え
delegateは、次のような場面でよく使われます。
処理が終わったあとに呼び出すコールバック
ボタンのクリックなどのイベント処理
条件判定や変換処理の差し替え
共通処理の一部だけを外部から渡す設計
非同期処理の完了通知
たとえば、処理の前後にログを出しつつ、実際に実行する処理だけ外から渡すことができます。
C#static void ExecuteWithLog(Action action)
{
Console.WriteLine("開始");
action.Invoke();
Console.WriteLine("終了");
}
ExecuteWithLog(() =>
{
Console.WriteLine("メイン処理");
});
このようにdelegateを使うと、「いつ実行するか」と「何を実行するか」を分離できます。
3. delegate.Invokeの使い方|メソッドを呼び出す基本パターン
3-1. delegate.Invoke()の基本構文
delegate.Invoke()の基本構文は次のとおりです。
C#delegate変数.Invoke(引数);
具体例を見てみましょう。
C#Action<string> show = message =>
{
Console.WriteLine(message);
};
show.Invoke("Hello Invoke");
Action<string>は、stringを1つ受け取り戻り値がないdelegateです。show.Invoke("Hello Invoke")を実行すると、showに登録されたラムダ式が呼び出されます。
3-2. del()とdel.Invoke()の違い
delegateは、Invoke()を書かずに直接呼び出すこともできます。
C#Action<string> show = message => Console.WriteLine(message);
show("Hello");
show.Invoke("Hello");
この2つは、基本的に同じ意味です。C#仕様でも、delegateのInvokeメソッドを呼び出すことは、delegate呼び出し構文を使うことと意味的に同等とされています。Microsoft Learn
実務では、短く書けるためshow("Hello")のように書かれることが多いです。一方で、?.Invoke()を使ってnullチェックを兼ねたい場合や、「delegateを呼び出している」と明示したい場合はInvokeがよく使われます。
C#show?.Invoke("Hello");
3-3. 引数を渡してInvokeする方法
引数ありのdelegateでは、定義した型に合わせて引数を渡します。
C#delegate void UserRegisteredHandler(string userName, int age);
class Program
{
static void Main()
{
UserRegisteredHandler handler = OnUserRegistered;
handler.Invoke("山田太郎", 30);
}
static void OnUserRegistered(string userName, int age)
{
Console.WriteLine($"{userName}さん、{age}歳を登録しました");
}
}
引数の数や型が一致しない場合、通常のdelegate呼び出しではコンパイルエラーになります。C#のdelegateは型安全なので、実行前に多くのミスを防げます。
3-4. 戻り値を受け取るInvokeの書き方
戻り値があるdelegateでは、Invoke()の戻り値を変数で受け取れます。
C#Func<int, int, int> add = (a, b) => a + b;
int result = add.Invoke(10, 20);
Console.WriteLine(result); // 30
Func<int, int, int>では、最初の2つのintが引数、最後のintが戻り値です。戻り値を使うdelegateでは、ActionではなくFuncを使うと簡潔です。
3-5. nullチェックには?.Invoke()を使う
delegate変数がnullの状態でInvoke()すると、NullReferenceExceptionが発生します。
C#Action? completed = null;
// completed.Invoke(); // NullReferenceException
安全に呼び出すには、null条件演算子を使って?.Invoke()と書きます。
C#completed?.Invoke();
これは「completedがnullでなければInvokeする」という意味です。イベント発火でもよく使われます。
C#public event EventHandler? Saved;
private void OnSaved()
{
Saved?.Invoke(this, EventArgs.Empty);
}
以前は、スレッドセーフ性を意識してローカル変数にコピーしてから呼び出す書き方がよく使われていました。
C#var handler = Saved;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
現在は、通常のイベント発火では?.Invoke()が簡潔で読みやすい書き方として広く使われます。
3-6. 複数メソッドを登録したdelegateをInvokeしたときの動き
delegateには、+=を使って複数のメソッドを登録できます。これをマルチキャストdelegateと呼びます。
C#Action action = First;
action += Second;
action += Third;
action.Invoke();
static void First() => Console.WriteLine("First");
static void Second() => Console.WriteLine("Second");
static void Third() => Console.WriteLine("Third");
実行結果は次のようになります。
First
Second
Third
複数のメソッドが登録されたdelegateを呼び出すと、登録された順番に実行されます。戻り値があるdelegateの場合、最終的に受け取れる戻り値は、呼び出しリストの最後のメソッドの戻り値です。C#仕様でも、出力パラメーターや戻り値の最終値は、呼び出しリスト内の最後のdelegate呼び出しから得られると説明されています。Microsoft Learn
C#Func<int> func = () => 1;
func += () => 2;
func += () => 3;
int result = func.Invoke();
Console.WriteLine(result); // 3
ただし、途中のメソッドで例外が発生すると、後続のメソッドが実行されないことがあります。すべての処理を確実に実行したい場合は、GetInvocationList()で個別に取り出し、try-catchで囲んで実行する方法もあります。
4. Control.Invokeとは|WinFormsでよく出てくるInvoke(Delegate)の正体
4-1. Control.Invoke(Delegate)はUIスレッドで処理を実行するためのメソッド
WinFormsで出てくるControl.Invoke(Delegate)は、delegateそのもののInvoke()とは目的が違います。
Control.Invokeは、指定したdelegateを「そのコントロールの基になるウィンドウハンドルを所有しているスレッド」で実行します。MicrosoftのControl.Invokeドキュメントでも、指定されたdelegateをコントロールの基になるウィンドウハンドルを所有するスレッドで実行する例が示されています。Microsoft Learn
C#this.Invoke(new Action(() =>
{
label1.Text = "UIスレッドで更新";
}));
このコードは、label1.Textの更新処理をUIスレッドに渡して実行しています。
4-2. 「コントロールを作成したスレッド」とは何か
WinFormsアプリでは、通常、フォームやボタン、ラベル、テキストボックスなどのUIコントロールはメインのUIスレッドで作成されます。
C#Application.Run(new MainForm());
このUIスレッドは、ユーザー操作、画面描画、イベント処理などを担当します。LabelやTextBoxなどのコントロールは、自分を作成したスレッドと強く結びついています。そのため、別スレッドから直接label1.Text = "..."のように操作すると問題が起きる可能性があります。
4-3. なぜ別スレッドからUIを直接操作できないのか
Windows Formsのコントロールへのアクセスは本質的にスレッドセーフではありません。複数のスレッドが同じコントロールを同時に操作すると、競合状態、デッドロック、フリーズ、ハングなどにつながる可能性があります。Microsoftの解説でも、Windows Formsコントロールへのアクセスは本質的にスレッドセーフではなく、クロススレッド操作では複雑なバグが発生し得ると説明されています。Microsoft Learn
危険な例は次のとおりです。
C#Task.Run(() =>
{
label1.Text = "別スレッドから更新"; // NG
});
デバッグ中には、次のような例外が発生することがあります。
InvalidOperationException:
Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.
日本語環境では、「スレッド間操作は無効です。作成されたスレッド以外のスレッドからアクセスされる制御。」のようなメッセージが表示されることがあります。Microsoftのドキュメントでも、Visual Studioデバッガーは安全でないクロススレッド呼び出しをInvalidOperationExceptionで検出すると説明されています。Microsoft Learn
4-4. Invokeが必要になる典型例:Task・Thread・BackgroundWorkerからの画面更新
Control.Invokeが必要になる典型例は、バックグラウンド処理から画面を更新するときです。
C#private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(1000);
this.Invoke(new Action(() =>
{
label1.Text = "処理完了";
}));
});
}
Task.Runの中身は通常UIスレッドとは別のスレッドで実行されます。そのため、label1.Textを直接変更するのではなく、this.Invokeを使ってUIスレッドに更新処理を渡します。
ThreadやBackgroundWorkerを使っている場合も同じです。重い処理はバックグラウンドで実行し、画面更新だけをUIスレッドに戻す、という考え方が基本です。
4-5. InvokeRequiredでUIスレッドかどうかを判定する
InvokeRequiredは、現在のスレッドからそのコントロールを直接操作してよいかを判定するために使います。
C#private void SetLabelText(string text)
{
if (label1.InvokeRequired)
{
label1.Invoke(new Action<string>(SetLabelText), text);
return;
}
label1.Text = text;
}
InvokeRequiredがtrueなら、現在のスレッドはUIスレッドではないため、InvokeでUIスレッドに処理を渡します。falseなら、そのままUIを更新します。MicrosoftのWinForms解説でも、InvokeRequiredはコントロールの作成スレッドIDと呼び出し元スレッドIDを比較し、異なる場合はControl.Invokeを呼び出す必要があると説明されています。Microsoft Learn
ただし、InvokeRequiredが常に万能というわけではありません。コントロールのハンドルがまだ作成されていない場合、別スレッドからの呼び出しでもfalseが返る可能性があります。そのため、バックグラウンドスレッドからフォーム初期化前のコントロールを触らないようにし、必要に応じてIsHandleCreatedも確認します。MicrosoftのInvokeRequiredドキュメントでも、ハンドル未作成時にはfalseが返る場合があり、その場合に単純にプロパティやメソッドを呼ぶべきではないと説明されています。Microsoft Learn
4-6. Label・TextBox・ProgressBarをInvokeで更新するサンプル
次の例では、バックグラウンド処理からLabel、TextBox、ProgressBarを安全に更新します。
C#private void buttonStart_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
UpdateProgress(i);
}
});
}
private void UpdateProgress(int value)
{
if (this.IsDisposed || !this.IsHandleCreated)
{
return;
}
if (this.InvokeRequired)
{
this.Invoke(new Action<int>(UpdateProgress), value);
return;
}
labelStatus.Text = $"{value}% 完了";
textBoxLog.AppendText($"{value}% 完了{Environment.NewLine}");
progressBar1.Value = value;
}
ポイントは、UIコントロールを更新する処理をUpdateProgressにまとめ、必要な場合だけInvokeでUIスレッドに戻していることです。これにより、呼び出し元がUIスレッドでもバックグラウンドスレッドでも安全に使いやすくなります。
5. Invoke・BeginInvoke・InvokeAsyncの違いと使い分け
5-1. Invokeは同期実行:完了するまで呼び出し元が待つ
Control.Invokeは同期実行です。つまり、呼び出し元のスレッドは、UIスレッドで処理が完了するまで待ちます。
C#this.Invoke(new Action(() =>
{
label1.Text = "更新";
}));
// UI更新が終わってからここに進む
Console.WriteLine("更新完了");
Microsoftの解説でも、Control.InvokeはUIスレッドのメッセージキューにdelegateを同期的に送信し、呼び出し元スレッドはUIスレッドが処理するまで待機すると説明されています。Microsoft Learn
同期実行は、UI更新が完了してから次の処理へ進みたい場合に便利です。ただし、UIスレッドが別の処理で待機していると、デッドロックやフリーズの原因になることがあります。
5-2. BeginInvokeは非同期実行:呼び出し元を止めずに処理する
Control.BeginInvokeは、UIスレッドに処理を依頼してすぐに戻ります。呼び出し元スレッドは、UI更新の完了を待ちません。
C#this.BeginInvoke(new Action(() =>
{
label1.Text = "非同期で更新";
}));
// UI更新の完了を待たずに進む
Console.WriteLine("BeginInvoke呼び出し後");
BeginInvokeは、呼び出し元をブロックしたくない場合に向いています。ログ表示、進捗表示、細かいUI更新など、完了結果をすぐに必要としない場合に使いやすい方法です。
5-3. InvokeAsyncはasync/awaitと相性がよいUI更新方法
.NET 9以降のWinFormsでは、Control.InvokeAsyncが利用できます。Control.InvokeAsyncは、UIスレッドのメッセージキューに処理を非同期にポストし、呼び出し元スレッドをブロックせず、完了を待てるTaskを返します。MicrosoftのWinForms解説でも、.NET 9でControl.InvokeAsyncが導入され、Control.Invokeがブロックするのに対して、InvokeAsyncは非ブロッキングにUIスレッドへポストすると説明されています。Microsoft Learn
C#private async void button1_Click(object sender, EventArgs e)
{
await Task.Run(async () =>
{
await Task.Delay(1000);
await this.InvokeAsync(() =>
{
label1.Text = "InvokeAsyncで更新";
});
});
}
async/await中心のコードでは、InvokeよりもInvokeAsyncのほうが自然に書けます。例外の伝播やキャンセルにも対応しやすい点がメリットです。Microsoftの解説でも、InvokeAsyncはTaskで待機でき、例外伝播やCancellationTokenによるキャンセルをサポートすると説明されています。Microsoft Learn
5-4. UI更新ではどれを選ぶべきか
使い分けの目安は次のとおりです。
| メソッド | 実行方式 | 向いている場面 |
|---|---|---|
Invoke | 同期 | UI更新完了を待ちたい |
BeginInvoke | 非同期 | UI更新を投げて呼び出し元を止めたくない |
InvokeAsync | 非同期・await可能 | .NET 9以降でasync/awaitと組み合わせたい |
.NET Frameworkや古いWinFormsではInvokeまたはBeginInvokeを使うことが多いです。一方、.NET 9以降でasync/awaitを使うなら、InvokeAsyncを優先的に検討するとよいでしょう。
5-5. デッドロックを避けるための注意点
Control.Invokeは同期的に待機するため、使い方によってはデッドロックを起こす可能性があります。
たとえば、バックグラウンドスレッドがInvokeでUIスレッドの完了を待ち、UIスレッド側もそのバックグラウンド処理の完了を待っていると、双方が待ち合って止まってしまいます。
避けるための基本方針は次のとおりです。
UIスレッドで
.Wait()や.Resultを多用しないasync/awaitを使える場面では最後までawaitでつなぐ完了待ちが不要なら
BeginInvokeやInvokeAsyncを使うUIスレッドに渡す処理は短くする
重い処理を
Invoke内に書かない
Invoke内に時間のかかる処理を書くと、UIスレッドが塞がり、画面が固まったように見えます。Invokeは重い処理を速くするためではなく、UIスレッドで実行すべき短い処理を安全に渡すためのものです。
5-6. パフォーマンスと可読性を考えた使い分け
UI更新で大切なのは、必要以上にInvokeを細かく呼びすぎないことです。
悪い例です。
C#for (int i = 0; i < 10000; i++)
{
this.Invoke(new Action(() =>
{
textBoxLog.AppendText($"{i}{Environment.NewLine}");
}));
}
このように大量のInvokeを繰り返すと、UIスレッドへの切り替えが多くなり、パフォーマンスも可読性も悪くなります。
改善例です。
C#var lines = new List<string>();
for (int i = 0; i < 10000; i++)
{
lines.Add(i.ToString());
}
this.Invoke(new Action(() =>
{
textBoxLog.AppendText(string.Join(Environment.NewLine, lines));
}));
UI更新はまとめて行い、重い計算やデータ作成はバックグラウンド側で済ませるのが基本です。
6. 実践コードで理解するInvokeとdelegate
6-1. delegate.Invokeで処理を差し替えるサンプル
delegateを使うと、同じ処理の流れの中で一部の処理だけを差し替えられます。
C#delegate int DiscountRule(int price);
class Program
{
static void Main()
{
int price = 1000;
DiscountRule normalDiscount = p => p - 100;
DiscountRule campaignDiscount = p => p / 2;
Console.WriteLine(CalculatePrice(price, normalDiscount)); // 900
Console.WriteLine(CalculatePrice(price, campaignDiscount)); // 500
}
static int CalculatePrice(int price, DiscountRule discountRule)
{
return discountRule.Invoke(price);
}
}
CalculatePriceは、値引きルールの中身を知りません。渡されたdelegateをInvokeしているだけです。このようにすると、処理の差し替えが簡単になります。
6-2. Actionを引数に受け取ってInvokeするサンプル
戻り値が不要な処理を渡したい場合は、Actionが便利です。
C#static void Execute(Action action)
{
Console.WriteLine("前処理");
action.Invoke();
Console.WriteLine("後処理");
}
Execute(() =>
{
Console.WriteLine("実行したい処理");
});
ログ出力、トランザクション制御、リトライ処理、計測処理など、「共通処理の中に任意の処理を挟みたい」場面でよく使えます。
6-3. FuncをInvokeして戻り値を取得するサンプル
戻り値が必要な場合は、Funcを使います。
C#static TResult Measure<TResult>(Func<TResult> func)
{
var start = DateTime.Now;
TResult result = func.Invoke();
var end = DateTime.Now;
Console.WriteLine($"処理時間: {(end - start).TotalMilliseconds}ms");
return result;
}
int total = Measure(() =>
{
return Enumerable.Range(1, 100).Sum();
});
Console.WriteLine(total);
Measureメソッドは、渡された処理を実行し、その戻り値を呼び出し元に返します。処理時間の計測や、共通的な例外処理を組み込みたいときに便利です。
6-4. イベント発火で?.Invoke()を使うサンプル
イベントはdelegateをベースにした仕組みです。イベントを発火するときは、登録されているハンドラーがない可能性を考慮して?.Invoke()を使います。
C#class Downloader
{
public event EventHandler? Completed;
public void Download()
{
Console.WriteLine("ダウンロード中...");
// 処理完了
Completed?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var downloader = new Downloader();
downloader.Completed += (sender, e) =>
{
Console.WriteLine("完了イベントを受け取りました");
};
downloader.Download();
}
}
Completedに誰も登録していない場合、Completedはnullです。その状態でCompleted.Invoke(...)を呼ぶと例外になりますが、Completed?.Invoke(...)ならnullのときは何もせず安全に通過します。
6-5. 別スレッドからWinFormsのUIを安全に更新するサンプル
WinFormsで別スレッドからUIを更新する基本形は次のとおりです。
C#private void buttonRun_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 1; i <= 5; i++)
{
Thread.Sleep(1000);
SetStatus($"{i}秒経過");
}
});
}
private void SetStatus(string message)
{
if (this.IsDisposed || !this.IsHandleCreated)
{
return;
}
if (this.InvokeRequired)
{
this.Invoke(new Action<string>(SetStatus), message);
return;
}
labelStatus.Text = message;
}
このパターンにしておくと、SetStatusをUIスレッドから呼んでも、バックグラウンドスレッドから呼んでも安全に動作しやすくなります。
6-6. async/awaitとInvokeを組み合わせるサンプル
.NET FrameworkやInvokeAsyncが使えない環境では、async/awaitとInvokeを組み合わせてUI更新することがあります。
C#private async void buttonRun_Click(object sender, EventArgs e)
{
labelStatus.Text = "開始";
string result = await Task.Run(() =>
{
Thread.Sleep(2000);
return "処理結果";
});
this.Invoke(new Action(() =>
{
labelStatus.Text = result;
}));
}
ただし、await後にUIスレッドへ戻る状況では、そもそもInvokeが不要な場合もあります。
C#private async void buttonRun_Click(object sender, EventArgs e)
{
labelStatus.Text = "開始";
string result = await Task.Run(() =>
{
Thread.Sleep(2000);
return "処理結果";
});
// 通常、WinFormsのイベントハンドラーではawait後にUIスレッドへ戻る
labelStatus.Text = result;
}
一方、Task.Runの内部から直接UIを更新する場合は、InvokeやInvokeAsyncが必要です。
C#private async void buttonRun_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
this.Invoke(new Action(() =>
{
labelStatus.Text = "Task.Runの中から更新";
}));
});
}
.NET 9以降なら、次のようにInvokeAsyncを使うと自然です。
C#private async void buttonRun_Click(object sender, EventArgs e)
{
await Task.Run(async () =>
{
await this.InvokeAsync(() =>
{
labelStatus.Text = "InvokeAsyncで更新";
});
});
}
7. Invokeとdelegateでよくあるエラーと対処法
7-1. NullReferenceException:delegateがnullのままInvokeしている
原因は、delegate変数にメソッドが代入されていない状態でInvoke()していることです。
C#Action? action = null;
action.Invoke(); // NullReferenceException
対処法は、?.Invoke()を使うことです。
C#action?.Invoke();
または、明示的にnullチェックします。
C#if (action != null)
{
action.Invoke();
}
イベント発火では特に?.Invoke()がよく使われます。
C#Completed?.Invoke(this, EventArgs.Empty);
7-2. InvalidOperationException:別スレッドからUIコントロールを操作している
WinFormsで次のように書くと、クロススレッド操作の例外が発生することがあります。
C#Task.Run(() =>
{
label1.Text = "更新"; // NG
});
対処法は、Control.InvokeやInvokeAsyncを使ってUIスレッドに処理を渡すことです。
C#Task.Run(() =>
{
this.Invoke(new Action(() =>
{
label1.Text = "更新";
}));
});
Windows Formsコントロールは、作成元ではないスレッドから直接呼び出すのが安全ではないため、別スレッドから操作する場合はControl.InvokeやControl.InvokeAsyncなどのマーシャリング手段を使います。Microsoft Learn
7-3. TargetParameterCountException:Invokeに渡す引数の数が違う
通常の厳密なdelegate呼び出しでは、引数の数が違うとコンパイル時に検出されます。
C#Action<string> action = message => Console.WriteLine(message);
// action.Invoke(); // コンパイルエラー
一方、DynamicInvokeやControl.Invokeのように、実行時にobject[]で引数を渡すケースでは、引数の数や型の不一致が実行時エラーになることがあります。
C#Delegate del = new Action<string>(message => Console.WriteLine(message));
del.DynamicInvoke(); // 引数不足で例外
Control.Invokeのオーバーロードでも、delegateに渡す引数はargsで指定します。指定する引数は、delegateが要求する数と型に合わせる必要があります。MicrosoftのControl.Invokeドキュメントでも、argsは指定したメソッドへ渡す引数配列であり、戻り値はdelegateの戻り値を含むObject、戻り値がない場合はnullと説明されています。Microsoft Learn
C#this.Invoke(new Action<string>(SetText), "Hello");
7-4. InvalidCastException:戻り値の型変換を間違えている
Control.InvokeやDynamicInvokeは戻り値をobjectとして返すため、キャストを間違えるとInvalidCastExceptionが発生します。
C#object result = this.Invoke(new Func<int>(() => 123));
string text = (string)result; // InvalidCastException
正しくは、実際の戻り値の型に合わせてキャストします。
C#object result = this.Invoke(new Func<int>(() => 123));
int number = (int)result;
戻り値の型が明確なら、Func<T>の型と受け取り側の型をそろえることが重要です。
7-5. ObjectDisposedException:破棄済みコントロールにInvokeしている
フォームを閉じたあとにバックグラウンド処理が残っていると、破棄済みのコントロールに対してInvokeしてしまうことがあります。
C#Task.Run(() =>
{
Thread.Sleep(3000);
this.Invoke(new Action(() =>
{
label1.Text = "更新";
}));
});
フォームが閉じられた後にこの処理が動くと、ObjectDisposedExceptionが発生する可能性があります。
対処法は、IsDisposedやIsHandleCreatedを確認することです。
C#private void SafeInvoke(Action action)
{
if (this.IsDisposed || !this.IsHandleCreated)
{
return;
}
if (this.InvokeRequired)
{
this.Invoke(action);
}
else
{
action.Invoke();
}
}
ただし、チェック後に破棄される競合もあり得るため、必要に応じてtry-catchも検討します。
C#private void SafeInvoke(Action action)
{
try
{
if (this.IsDisposed || !this.IsHandleCreated)
{
return;
}
if (this.InvokeRequired)
{
this.Invoke(action);
}
else
{
action();
}
}
catch (ObjectDisposedException)
{
// フォーム終了中なら無視するなど、アプリの方針に合わせる
}
}
7-6. コンパイルエラー:Delegate型と具体的なdelegate型を混同している
System.Delegateはすべてのdelegate型の基底クラスですが、具体的な引数や戻り値の情報を持つdelegate型とは扱いが違います。
C#Delegate del = new Action<string>(message => Console.WriteLine(message));
// del.Invoke("Hello"); // そのままでは呼べない
Delegate型として保持している場合は、具体的な型にキャストするか、DynamicInvokeを使う必要があります。
C#Delegate del = new Action<string>(message => Console.WriteLine(message));
((Action<string>)del).Invoke("Hello");
ただし、安易にDelegate型で扱うと型安全性が下がります。通常は、Action、Func、独自delegate型など、具体的なdelegate型で受け取るほうが安全です。
C#static void Execute(Action<string> action)
{
action.Invoke("Hello");
}
7-7. エラーを防ぐための安全なInvokeテンプレート
WinFormsでUI更新を安全に行うためのテンプレートは次のように書けます。
C#private void UI(Action action)
{
if (this.IsDisposed || !this.IsHandleCreated)
{
return;
}
try
{
if (this.InvokeRequired)
{
this.Invoke(action);
}
else
{
action.Invoke();
}
}
catch (ObjectDisposedException)
{
// フォーム終了中など
}
}
使い方は次のとおりです。
C#Task.Run(() =>
{
UI(() =>
{
labelStatus.Text = "安全に更新";
progressBar1.Value = 50;
});
});
.NET 9以降でInvokeAsyncが使える場合は、非同期版も検討できます。
C#private async Task UIAsync(Action action)
{
if (this.IsDisposed || !this.IsHandleCreated)
{
return;
}
if (this.InvokeRequired)
{
await this.InvokeAsync(action);
}
else
{
action.Invoke();
}
}
InvokeAsyncはasync/awaitと組み合わせやすく、呼び出し元をブロックせずにUIスレッドへ処理を渡せるため、非同期処理の多いWinFormsアプリで有効です。Microsoft Learn
8. Invokeとdelegateを使うべき場面・使わない方がよい場面
8-1. Invokeを使うべき場面:UIスレッドへの処理移譲
Control.Invokeを使うべき代表的な場面は、別スレッドからWinFormsのUIを更新するときです。
C#Task.Run(() =>
{
var message = LoadMessage();
this.Invoke(new Action(() =>
{
label1.Text = message;
}));
});
バックグラウンドで重い処理を行い、結果を画面に反映する部分だけUIスレッドで実行する、という分担が基本です。
8-2. delegateを使うべき場面:処理の受け渡し・コールバック・イベント
delegateを使うべき場面は、処理そのものを外から渡したいときです。
C#static void Retry(Action action, int count)
{
for (int i = 0; i < count; i++)
{
try
{
action.Invoke();
return;
}
catch
{
if (i == count - 1)
{
throw;
}
}
}
}
呼び出し側は、任意の処理を渡せます。
C#Retry(() =>
{
Console.WriteLine("失敗するかもしれない処理");
}, 3);
このようにdelegateを使うと、共通処理と個別処理を分離できます。
8-3. ActionやFuncで十分な場面
独自のdelegate型を宣言しなくても、ActionやFuncで十分な場面は多いです。
C#static void Execute(Action action)
{
action();
}
static int Calculate(Func<int, int, int> func)
{
return func(10, 20);
}
特に、処理の意味がメソッド名や引数名で十分伝わる場合は、ActionやFuncで問題ありません。
一方、ドメイン上の意味を型名で明確にしたい場合は、独自delegate型を定義してもよいでしょう。
C#delegate bool UserValidationRule(User user);
このように書くと、「ユーザー検証ルール」を表すdelegateであることが読み手に伝わりやすくなります。
8-4. eventを使った方がよい場面
外部に「何かが起きた」ことを通知したい場合は、delegate変数をそのまま公開するよりもeventを使うべきです。
C#public event EventHandler? Completed;
eventにすると、外部からできる操作は基本的に+=と-=による購読・解除に制限されます。外部から勝手にイベントを発火されたり、登録済みハンドラーを丸ごと上書きされたりするリスクを減らせます。
悪い例です。
C#public Action? Completed;
この場合、外部から次のように上書きできてしまいます。
C#obj.Completed = null;
イベント通知を表すなら、eventを使うほうが安全です。
8-5. async/awaitで置き換えられる場面
以前は、完了通知のためにdelegateやコールバックを渡す設計がよく使われていました。
C#void Load(Action<string> completed)
{
string result = "結果";
completed.Invoke(result);
}
現在のC#では、非同期処理の完了を表すならTaskとasync/awaitのほうが自然な場合があります。
C#async Task<string> LoadAsync()
{
await Task.Delay(1000);
return "結果";
}
string result = await LoadAsync();
処理結果を一度返すだけなら、delegateコールバックよりTask<T>のほうが読みやすくなります。一方、途中経過の通知や複数回のイベント通知には、delegateやeventが適している場合もあります。
8-6. Invokeを多用すると読みにくくなる理由
Invokeを多用すると、処理の流れが追いにくくなります。
C#this.Invoke(new Action(() =>
{
button1.Enabled = false;
}));
this.Invoke(new Action(() =>
{
label1.Text = "処理中";
}));
this.Invoke(new Action(() =>
{
progressBar1.Value = 10;
}));
このような場合は、まとめたほうが読みやすくなります。
C#this.Invoke(new Action(() =>
{
button1.Enabled = false;
label1.Text = "処理中";
progressBar1.Value = 10;
}));
さらに、UI更新専用のメソッドを用意すると意図が明確になります。
C#private void ShowProcessing()
{
button1.Enabled = false;
label1.Text = "処理中";
progressBar1.Value = 10;
}
Invokeは便利ですが、あくまでスレッド間の境界を越えるための手段です。ビジネスロジックや重い処理をInvoke内に詰め込むと、UIコードが肥大化し、保守しづらくなります。
9. 初心者が混同しやすいポイント
9-1. InvokeはC#のキーワードではなくメソッド名
InvokeはC#の予約語やキーワードではありません。delegate型に用意されるメソッド名であり、WinFormsのControlクラスにもInvokeというメソッドがあります。
そのため、次のように文脈によって意味が変わります。
C#action.Invoke(); // delegateのInvoke
this.Invoke(action); // ControlのInvoke
名前は同じでも、呼び出している対象が違えば役割も違います。
9-2. delegateは処理そのものではなく処理を参照する型
delegateは、処理そのものというより、「処理を参照するための型」です。
C#Action action = DoWork;
この時点でDoWorkが実行されるわけではありません。実行されるのは、次のように呼び出したときです。
C#action.Invoke();
代入と実行を混同しないことが重要です。
9-3. delegate.InvokeとControl.Invokeを同じ意味で考えない
delegate.Invokeは、delegateに登録されているメソッドを呼び出します。
C#Action action = () => Console.WriteLine("Hello");
action.Invoke();
Control.Invokeは、WinFormsのUIスレッドでdelegateを実行します。
C#this.Invoke(new Action(() =>
{
label1.Text = "Hello";
}));
Control.Invokeの引数としてdelegateを渡すため、見た目は似ています。しかし、Control.Invokeの主目的は「UIスレッドへの処理の移譲」です。
9-4. UI更新のInvokeは「別スレッド処理を速くする」ためではない
Invokeを使うと、別スレッド処理が速くなるわけではありません。
むしろ、Control.InvokeはUIスレッドで処理を実行するため、重い処理をInvoke内に書くとUIが固まります。
悪い例です。
C#this.Invoke(new Action(() =>
{
// NG: UIスレッドで重い処理をしている
Thread.Sleep(5000);
label1.Text = "完了";
}));
良い例です。
C#Task.Run(() =>
{
// 重い処理はバックグラウンドで行う
Thread.Sleep(5000);
// UI更新だけInvokeする
this.Invoke(new Action(() =>
{
label1.Text = "完了";
}));
});
Invokeは、UIスレッドでしかできない短い処理を安全に実行するためのものです。
9-5. InvokeRequiredが常にtrueになるわけではない
別スレッドから呼んだからといって、常にInvokeRequiredがtrueになるとは限りません。コントロールのハンドルがまだ作成されていない場合、InvokeRequiredがfalseを返すことがあります。Microsoftのドキュメントでも、適切なハンドルが見つからない場合にはInvokeRequiredがfalseを返ることがあり、そのままプロパティやメソッドを呼ぶべきではないと説明されています。Microsoft Learn
そのため、フォームが表示される前にバックグラウンド処理を開始してUIを触る設計は避けるべきです。必要であれば、フォームのShownイベント後に処理を開始するなど、コントロールの準備が整ってから操作します。
9-6. ラムダ式を使うとdelegateの記述を大幅に短くできる
昔ながらのdelegate記法では、次のように書きます。
C#this.Invoke(new Action(UpdateLabel));
void UpdateLabel()
{
label1.Text = "更新";
}
ラムダ式を使うと、短く書けます。
C#this.Invoke(new Action(() =>
{
label1.Text = "更新";
}));
さらにAction型が推論できる場面では、より簡潔に書けることもあります。
C#Action action = () => label1.Text = "更新";
action.Invoke();
短いUI更新や一時的なコールバックでは、ラムダ式を使うとコードの見通しがよくなります。
10. Invokeとdelegateの理解を深める関連知識
10-1. イベントとdelegateの関係
C#のイベントは、delegateを基盤にした仕組みです。イベントは「外部から購読できる通知口」のようなものです。
C#public event EventHandler? Clicked;
イベントを発火するときは、内部でdelegateを呼び出します。
C#Clicked?.Invoke(this, EventArgs.Empty);
つまり、イベントを理解するにはdelegateの理解が欠かせません。ボタンのクリックイベント、フォームのロードイベント、独自クラスの完了通知など、多くのイベント処理はdelegateの考え方の上に成り立っています。
10-2. マルチキャストdelegateの仕組み
delegateには複数のメソッドを登録できます。
C#Action action = () => Console.WriteLine("A");
action += () => Console.WriteLine("B");
action += () => Console.WriteLine("C");
action.Invoke();
実行結果です。
A
B
C
イベントに複数のイベントハンドラーを登録できるのも、このマルチキャストdelegateの仕組みがあるためです。
C#button1.Click += Handler1;
button1.Click += Handler2;
戻り値があるマルチキャストdelegateでは、最後に実行されたメソッドの戻り値だけが呼び出し元に返ります。そのため、戻り値を使う処理に複数メソッドを登録する場合は注意が必要です。
10-3. スレッドセーフなイベント発火
イベント発火では、購読者がいない可能性と、別スレッドで購読解除される可能性を考える必要があります。現在よく使われる書き方は次の形です。
C#Completed?.Invoke(this, EventArgs.Empty);
より明示的に書くなら、ローカル変数にコピーします。
C#var handler = Completed;
handler?.Invoke(this, EventArgs.Empty);
この書き方にすると、nullチェック後に別スレッドで購読解除されても、ローカル変数にコピーしたdelegateを安全に呼び出せます。
10-4. SynchronizationContextとの関係
WinFormsやWPFなどのUIアプリでは、UIスレッドに処理を戻す仕組みとしてSynchronizationContextが関係します。
async/awaitでは、通常、await前のコンテキストを捕捉し、await後に元のUIスレッドへ戻ろうとします。そのため、WinFormsのイベントハンドラー内で次のように書くと、await後はUIスレッドに戻っていることが多いです。
C#private async void button1_Click(object sender, EventArgs e)
{
string result = await LoadAsync();
label1.Text = result; // 多くの場合、Invoke不要
}
ただし、Task.Runの内部や、ConfigureAwait(false)を使った後など、UIスレッドに戻らない場面では、InvokeやInvokeAsyncが必要になることがあります。
10-5. WPFのDispatcher.Invokeとの違い
WPFでは、WinFormsのControl.Invokeではなく、Dispatcher.InvokeやDispatcher.BeginInvoke、Dispatcher.InvokeAsyncを使います。
WinFormsの例です。
C#this.Invoke(new Action(() =>
{
label1.Text = "WinForms";
}));
WPFの例です。
C#Dispatcher.Invoke(() =>
{
label.Content = "WPF";
});
目的は似ていて、どちらもUIスレッドでUI更新を行うための仕組みです。ただし、使うクラスやAPIはフレームワークによって異なります。
10-6. TaskSchedulerやasync/awaitとの使い分け
TaskScheduler.FromCurrentSynchronizationContext()を使うと、現在の同期コンテキストに紐づいたスケジューラを取得できます。古いTaskベースのコードでは、UIスレッドに戻すために使われることがあります。
C#var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Run(() =>
{
return "結果";
})
.ContinueWith(task =>
{
label1.Text = task.Result;
}, scheduler);
ただし、現在はasync/awaitで書いたほうが読みやすい場合が多いです。
C#private async void button1_Click(object sender, EventArgs e)
{
string result = await Task.Run(() =>
{
return "結果";
});
label1.Text = result;
}
UI更新のためだけに複雑なdelegateやTaskSchedulerを使うより、async/await、InvokeAsync、必要最小限のInvokeを使い分けるほうが保守しやすくなります。
まとめ
C#のInvokeとdelegateを理解するには、まず「delegateのInvoke」と「WinFormsのControl.Invoke」を分けて考えることが重要です。
delegate.Invoke()は、delegateに登録されたメソッドを実行するための呼び出しです。del()とdel.Invoke()は基本的に同じ意味で、nullの可能性がある場合は?.Invoke()を使うと安全です。delegateを使うと、メソッドを変数のように扱い、コールバック、イベント、処理の差し替えなどを柔軟に実装できます。
一方、WinFormsのControl.Invokeは、UIスレッドで処理を実行するためのメソッドです。TaskやThreadなどのバックグラウンド処理からLabel、TextBox、ProgressBarなどを更新する場合、直接操作するとクロススレッド例外や不安定な動作につながるため、Invoke、BeginInvoke、InvokeAsyncを使ってUIスレッドに処理を渡します。
使い分けの基本は、UI更新の完了を待ちたいならInvoke、呼び出し元を止めたくないならBeginInvoke、.NET 9以降でasync/awaitと組み合わせるならInvokeAsyncです。Invokeは便利ですが、多用すると処理の流れが読みにくくなるため、UI更新は短くまとめ、重い処理はバックグラウンド側で実行することが大切です。
c# invoke delegateで迷ったときは、次のように考えると整理しやすくなります。
delegateを実行したいだけなら
delegate.Invoke()またはdel()nullの可能性があるなら
?.Invoke()WinFormsで別スレッドからUIを更新するなら
Control.Invokeasync/await中心で.NET 9以降なら
Control.InvokeAsync通知の仕組みを作るならdelegateより
eventを検討する独自delegateが不要なら
ActionやFuncを使う
この切り分けができれば、C#のInvokeとdelegateは難しいものではありません。delegateは「処理を渡すための型」、Invokeは「その処理を実行するための呼び出し」、Control.Invokeは「UIスレッドに処理を渡すための仕組み」と覚えておくと、コードの意味を正しく読み取れるようになります。

