Menu

C#のデリゲート:宣言、呼び出し、マルチキャスト、コールバック

C#のデリゲートとは何か、どう使うかを解説します。デリゲート型の宣言、メソッド、ラムダ、匿名メソッドからの作成、呼び出し、コールバックとしてのデリゲート、+=と-=によるマルチキャストデリゲート、?.Invokeによるnullチェック、そしてFunc、Action、イベントとの関係を扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

デリゲートは、メソッドのシグネチャを表す型です。デリゲート型の変数はそのシグネチャを持つメソッドへの参照を保持し、変数を呼び出すとそのメソッドが呼ばれます。これがC#で関数を値として扱う方法で、関数を格納したり、他のメソッドに渡したり、後で呼び出したりできます。

出力:

40
70
68.00

delegate decimal PriceRule(decimal price); は、class と同じように新しい型を宣言します。decimal を1つ受け取って decimal を返すメソッドなら、どれでも当てはまります。変数 rule は3つの異なるメソッドを順に指し、各呼び出しはそのとき保持していたメソッドを実行しました。

宣言、作成、呼び出し

デリゲート型の宣言は、メソッドのシグネチャの前に delegate キーワードを付けた形です。通常は名前空間のレベルでクラスと並べて置くか、クラスの中に入れ子にします。

デリゲートのインスタンスを作る方法は4つあります。

PriceRule a = HalfPrice;                             // method group (most common)
PriceRule b = new PriceRule(HalfPrice);              // explicit constructor, same thing
PriceRule c = p => p - 5m;                            // lambda expression
PriceRule d = delegate (decimal p) { return p; };    // anonymous method (C# 2 syntax)

呼び出す方法は2つあり、どちらも同じです:rule(80m) と rule.Invoke(80m)。コンパイラは代入時にシグネチャを確認するので、合わないメソッドは実行時の思わぬ問題ではなく、コンパイルエラーになります。

デリゲートはインスタンスメソッドも指せます。その場合、メソッドと、それを呼び出す対象のオブジェクトの両方を覚えています。

出力:

Ana got: Your order shipped
target: Mailbox, method: Receive

Target はデリゲートがメソッドを呼び出す対象のオブジェクト(静的メソッドなら null)で、Method はメソッドを表します。対象を保持するということは、デリゲートがそのオブジェクトを生かし続けるということでもあり、これはイベントで重要になります。

コールバックとしてのデリゲート

デリゲートの主な役割は、呼び出し側が選んだコードをメソッドから呼び出せるようにすることです。メソッドはいつ何が起きるかを決め、呼び出し側は何をするかを決めます。

出力:

[##        ] 25%
[#####     ] 50%
[#######   ] 75%
[##########] 100%
import finished

ImportRows はコンソールやバーについて何も知りません。呼び出し側が渡すものによって、同じメソッドがプログレスバーにも、ログの行にも、UIの更新にもなります。List.Sort(Comparison<T>)、Array.Find(Predicate<T>)、Task.Run(Action)、そしてすべての LINQ 演算子がこの形に従っています。

マルチキャストデリゲート:+=と-=

デリゲートは複数のメソッドを保持できます。+= は呼び出しリストにメソッドを追加し、-= は削除し、デリゲートを呼び出すとすべてのメソッドが順に呼ばれます。

出力:

email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002

実際には、マルチキャストデリゲートの3つの点が重要です。

  • 戻り値: デリゲート型が値を返す場合、マルチキャストデリゲートを呼び出して返るのは最後のメソッドの結果だけです。すべての結果を集めるには、GetInvocationList() をループして、それぞれを自分で呼び出します。
  • 例外: あるメソッドが例外を投げると、残りのメソッドは実行されず、例外は呼び出し側に届きます。
  • 不変性: デリゲートは不変です。+= はより長いリストを持つ新しいデリゲートを作って変数に代入し直します。そのため、コピーに対して -= で削除しても元のデリゲートには影響しません。

-= でラムダを削除できるのは、同じデリゲートのインスタンスを渡したときだけです。同じラムダを2回書くと2つの異なるデリゲートが作られるので、handlers -= id => Log(id); は何も削除しません。登録を解除するつもりなら、ラムダを変数に格納しておきます。

nullのデリゲートと?.Invoke

何も割り当てられていないデリゲート変数は null で、最後のメソッドを -= で削除した場合も null になります。null を呼び出すと NullReferenceException が投げられます。

出力:

no handler, no crash
saved draft.txt
NullReferenceException

?.Invoke は、省略可能なコールバックを呼び出す標準的な方法です。?. 演算子はかっこの直前に置けない(onSaved?("x") は正しい構文ではありません)ので、この形ではデリゲートの Invoke メソッドを名前で呼び出します。これは省略形の呼び出しと同じ動作です。

デリゲート、Func、Action、イベント

シグネチャごとにデリゲート型を宣言するのは面倒なので、.NETにはジェネリックなものが用意されています。

組み込みの型同等の独自の宣言
Actiondelegate void Action();
Action<T>delegate void Action<T>(T arg);
Func<TResult>delegate TResult Func<TResult>();
Func<T, TResult>delegate TResult Func<T, TResult>(T arg);
Predicate<T>delegate bool Predicate<T>(T obj);

最初の例の PriceRule は Func<decimal, decimal> と同じ形ですが、2つは別の型で、互いに変換されません。同じシグネチャを持つ2つの組み込みの型でさえ変換されません。

Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'

Predicate<int> ok = new Predicate<int>(isPositive);   // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x);              // or with a lambda

ラムダは互換性のある任意のデリゲート型に変換されるので、APIはたいていラムダを受け取り、この問題はめったに起きません。ジェネリックな型の一覧と、独自の宣言が今でも価値を持つ場面は、FuncとActionのページで扱います。

イベントはデリゲートの上に直接作られています。event フィールドは、外部のコードが += と -= しかできず、呼び出しや上書きはできないマルチキャストデリゲートです。上のマルチキャストの例の購読者リストこそ、イベントがその追加の保護とともに管理してくれるものです。イベントについては専用のページで扱います。

よくある質問

C#のデリゲートとは何ですか?

デリゲートは、メソッドへの型安全な参照です。delegate decimal PriceRule(decimal price); のようにシグネチャを付けてデリゲート型を宣言すると、そのシグネチャを持つ任意のメソッド(静的メソッド、インスタンスメソッド、ラムダ)をその型の変数に格納し、後でそれを通して呼び出せます。デリゲートは、C#で関数を値として渡すための仕組みです。

C#のデリゲートとFuncの違いは何ですか?

Func<T, TResult> と Action<T> は、.NETがあらかじめ宣言している、引数と戻り値の型についてジェネリックなデリゲート型です。独自の delegate 宣言は、型とその引数に役割を表す名前を付けられ、ref、out、in の引数にも対応します。ほとんどのコードでは Func と Action で十分です。

C#のマルチキャストデリゲートとは何ですか?

複数のメソッドを保持するデリゲートです。+= は呼び出しリストにメソッドを追加し、-= は削除します。デリゲートを呼び出すと、すべてのメソッドが順に呼ばれます。デリゲートが値を返す場合、呼び出し側が受け取るのは最後のメソッドの結果だけで、あるメソッドで例外が起きると残りは実行されません。

nullかもしれないデリゲートを安全に呼び出すには?

null条件演算子を使います:onProgress?.Invoke(50);。メソッドが何も割り当てられていないデリゲート変数は null で、直接呼び出すと NullReferenceException が投げられます。?.Invoke は変数を1回だけ読むので、別のスレッドが同時に最後のハンドラーを削除しても安全です。

FuncやActionではなく独自のデリゲート型を宣言すべきなのはどんなときですか?

シグネチャに ref、out、in の引数が必要なとき(FuncとActionでは表せません)、名前付きの型で公開APIがわかりやすくなるとき(delegate bool Validator(string input))、IntelliSenseに引数名を表示したいときです。それ以外では、.NETの開発者なら誰でも知っている Func と Action を優先します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める