デリゲートは、メソッドのシグネチャを表す型です。デリゲート型の変数はそのシグネチャを持つメソッドへの参照を保持し、変数を呼び出すとそのメソッドが呼ばれます。これが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にはジェネリックなものが用意されています。
| 組み込みの型 | 同等の独自の宣言 |
|---|---|
Action | delegate 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 を優先します。