Func と Action は、.NETがあらかじめ宣言しているジェネリックなデリゲート型なので、先にデリゲート型を宣言しなくても関数を格納したり渡したりできます。Func は値を返し、Action は何も返しません。
出力:
4
46.00
False
Hello!
Mia scored 87
覚えておくべきルールは、Func では最後の型引数が戻り値の型だということです。Func<int, int, bool> は2つの int を受け取って bool を返します。Func<int> は何も受け取らずに int を返します。Action の型引数はすべて引数です。
型の一覧
| 型 | 引数 | 戻り値 |
|---|---|---|
Action | なし | void |
Action<T> | T | void |
Action<T1, T2> ... 16個まで | T1, T2, ... | void |
Func<TResult> | なし | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... 16個まで | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int(負、ゼロ、正) |
Converter<TIn, TOut> | TIn | TOut |
Predicate、Comparison、Converter は古くからある型で、List<T> と Array のメソッドに現れます:FindAll(Predicate<T>)、Sort(Comparison<T>)、ConvertAll(Converter<T, TOut>)。LINQは一貫して Func を使うので、list.Where(...) と list.FindAll(...) は同じ形でも型の異なるラムダを受け取ります。
出力:
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
メソッドに関数を渡す
Func や Action の引数を使うと、アルゴリズムの変わる部分を呼び出し側が提供できます。よくある形は「これを実行して計測する」と「うまくいくまでこれを試す」の2つです。
出力:
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry はジェネリックなので、その Func<int, T> は何でも返せます。コンパイラはラムダから T を string と推論しました。Twice に渡したラムダはローカル変数 clicks を変更しています。これが動くのは、ラムダが値のコピーではなく変数そのものをキャプチャするからです。
メソッドグループ:名前でメソッドを渡す
Func や Action を埋めるのにラムダは必須ではありません。シグネチャが一致する既存のメソッドの名前(「メソッドグループ」)がそのまま変換されます。
出力:
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse) は、呼び出しが1層少ないだけで Select(s => int.Parse(s)) と同じです。コンパイラは対象の型に合うオーバーロードを選びます。int.Parse にはいくつかのオーバーロードがあり、Func<string, int> に一致するのは Parse(string) だけです。コンパイラが1つに決められないとき(複数のオーバーロードが同じ程度に当てはまる場合や、メソッドグループからジェネリックメソッドの型引数を推論できない場合)は呼び出しがコンパイルできず、呼び出しを明示するラムダで解決できます。
Func として使われたメソッドグループも、ラムダとまったく同じく、デリゲートが呼び出されたときに後から実行されます。違うのは書き方だけです。
関数を返す
メソッドは関数を組み立てて返すことができます。返された Func は、組み立てに使われた値を持ち運びます。
出力:
140
180
162
Twice(staff) は、社員割引を2回適用する新しい関数を組み立てます。200が180になり、さらに162になります。
関数の辞書
関数を辞書に格納すると、長い switch が参照表になります。コマンドハンドラー、電卓、メニューの動作によく使われる形で、実行時に新しいエントリを追加することもできます。
出力:
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
演算子を追加するには辞書のエントリを1つ増やすだけで、ループは変更しません。2つ目の辞書に渡した StringComparer.OrdinalIgnoreCase によって、コマンド名は大文字と小文字を区別しなくなります。
非同期関数:ActionではなくFunc<Task>
async のコードでは、デリゲート型によって、呼び出し側が処理の完了を待てるかどうかが決まります。
Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save(); // the caller can await it and see its exceptions
Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);
Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad(); // async void: fire and forget, exceptions escape
Action に代入された非同期のラムダは async void メソッドになります。何もそれをawaitできず、中で起きた例外は呼び出し側に届きません。例外はスレッドプール(デスクトップアプリならUIスレッド)で再スローされ、たいていプロセスをクラッシュさせます。非同期の処理を受け取るメソッドを書くときは、Func<Task>(または Func<Task<T>>)を受け取ります。
独自のデリゲートを宣言すべきとき
ほとんどの用途は Func と Action で足ります。次の場合は名前付きのデリゲート型を宣言します。
- シグネチャに
ref、out、inの引数がある。Funcではそれらを表せないので、TryParseのような関数にはdelegate bool TryParser<T>(string text, out T value);が必要です。 - 名前が公開APIでの役割を表す。 メソッドのシグネチャにある
delegate bool Validator(string input)はFunc<string, bool>より多くを語り、その引数名はIntelliSenseにも表示されます。 params引数が必要。 これもジェネリックデリゲートでは持てません。
独自のデリゲート型が今でも現れるもう1つの場所はイベントですが、ほとんどは組み込みの EventHandler<TEventArgs> で済みます。デリゲート型の宣言と組み合わせはデリゲートのページで扱います。
よくある質問
C#のFuncとActionの違いは何ですか?
Func は値を返し、Action は返しません。Func<int, string, bool> では、最後の型引数(bool)が戻り値の型で、それ以外は引数です。Action<int, string> は int と string を受け取り、void を返します。どちらも引数が0から16個までの版があります。
C#で関数を引数として渡すには?
引数を適切な形の Func か Action として宣言し、メソッドの中で呼び出します:static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);。呼び出し側はラムダ(Apply(80m, p => p * 0.9m))かメソッド名(Apply(80m, HalfPrice))を渡します。
C#のPredicate<T>とは何ですか?
T を受け取って bool を返すデリゲート型で、Func<T, bool> と同じ形です。Func より前からあり、List<T>.Find、FindAll、RemoveAll、Exists、Array.Find で使われています。2つの型は互いに変換されませんが、ラムダはどちらにも変換されます。
非同期のコードでFuncを使うには?
結果のない非同期関数には Func<Task>、結果のあるものには Func<T, Task<TResult>> を使い、呼び出し側が await できるようにします。非同期のラムダを Action に代入するのは避けます。async void になり、awaitできず、例外も呼び出し側でキャッチできなくなります。
Funcはoutやrefの引数を持てますか?
持てません。Func と Action のジェネリック引数は通常の値の引数なので、Func<string, out int, bool> と書く方法はありません。TryParse のようなシグネチャには、独自のデリゲートを宣言します:delegate bool TryParser<T>(string text, out T value);。