Menu

C#のFuncとAction:ジェネリックデリゲート、Predicate、使用例

C#のFuncとActionの仕組みを解説します。値を返す関数のためのFunc<T, TResult>、値を返さない関数のためのAction<T>、PredicateとComparison、関数の受け渡しと返却、コマンドの辞書、非同期のラムダ、そして独自のデリゲートを宣言すべき場面を扱います。

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

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>Tvoid
Action<T1, T2> ... 16個までT1, T2, ...void
Func<TResult>なしTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... 16個までT1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint(負、ゼロ、正)
Converter<TIn, TOut>TInTOut

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);。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める