FuncとActionは、.NETにあらかじめ用意された汎用のデリゲート型です。戻り値があるメソッドを入れるのがFunc、戻り値がない(void の)メソッドを入れるのがActionです。 デリゲートは「メソッドを入れておける型」、ラムダ式は「その場でメソッドを書く書き方」で、ラムダ式で書いた処理を Func や Action の変数に入れて使います。

この記事では、Action・Func・Predicate・Comparison・EventHandler の違いを表と図で整理し、.NET SDK 8.0.131(C# 12)で実行した結果を載せています。自作デリゲートからラムダ式までの書き換え、用途別のコピペ用サンプル、C# 10 の自然型、static ラムダ、非同期処理、ループ変数のキャプチャ、式ツリーとの違いまで、順に確認できます。

Func・Action・Predicateなどの早見表

型 引数 戻り値 同じ形のメソッド 代表的な使用箇所
Action なし なし void M() コールバック、Task.Run
Action<T> 1個(最大16個) なし void M(T x) List<T>.ForEach、通知処理
Func<TResult> なし あり TResult M() 遅延実行、Lazy<T>、ファクトリー
Func<T, TResult> 1個(最大16個) あり TResult M(T x) LINQ の Select・Where・OrderBy
Predicate<T> 1個 bool bool M(T x) List<T>.Find・FindAll・RemoveAll・Exists
Comparison<T> 2個(同じ型) int int M(T a, T b) List<T>.Sort、Array.Sort
EventHandler sender と EventArgs なし void M(object? s, EventArgs e) イベント
EventHandler<TEventArgs> sender とイベントデータ なし void M(object? s, TEventArgs e) データ付きのイベント
Func<Task> なし Task async Task M() 非同期のコールバック
Func<T, Task<TResult>> 1個 Task<TResult> async Task<TResult> M(T x) 非同期の変換、リトライ

覚え方は「Func の型引数は、最後の1つが戻り値」です。Func<int, string, bool> なら、int と string を受け取って bool を返します。Action は型引数がすべて引数です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# FuncとActionの使い方 デリゲート・ラムダ式との違いを図解」

デリゲート・Func・Action・ラムダ式の関係を図で見る

4つの言葉は、役割が違います。

flowchart TD
    D[デリゲート型<br/>メソッドを入れておける型] --> U[自作のデリゲート型<br/>delegate int Calc int a, int b]
    D --> S[標準のデリゲート型]
    S --> A[Action 系<br/>戻り値なし]
    S --> F[Func 系<br/>戻り値あり]
    S --> P[Predicate / Comparison / EventHandler<br/>用途が決まった型]
    L[ラムダ式<br/>a, b => a + b] -. 変数に代入できる .-> U
    L -. 変数に代入できる .-> A
    L -. 変数に代入できる .-> F
    L -. 変数に代入できる .-> P
    M[メソッド名<br/>Helpers.Add] -. 変数に代入できる .-> F
言葉 何を指すか 例
デリゲート メソッドを入れておける「型」の総称 delegate int Calc(int a, int b);
Func・Action 標準で用意された汎用のデリゲート型 Func<int, int, int>
ラムダ式 名前のないメソッドをその場で書く「書き方」 (a, b) => a + b
メソッドグループ 既存のメソッドを名前で渡す書き方 Helpers.Add

「Func か、ラムダ式か」は二者択一ではありません。 Func は入れ物(型)、ラムダ式は中身(値)の書き方なので、Func<int, int, int> add = (a, b) => a + b; のように一緒に使います。

この記事のコードの前提

  • .NET SDK 8.0.131(dotnet --version)、C# 12、dotnet new console で作ったプロジェクト
  • 既定の設定(ImplicitUsings と Nullable が有効)のまま、Program.cs に最上位ステートメントで記述
  • クラスやデリゲート型の宣言は、ファイルの末尾(最上位ステートメントの後)に置く
dotnet new console -o FuncSample
cd FuncSample
dotnet run

基本の使い方:ActionとFunc

Action hello = () => Console.WriteLine("hello");
Action<string, int> show = (name, n) => Console.WriteLine($"{name}: {n}");
Func<int> seven = () => 7;
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;
Comparison<string> byLength = (a, b) => a.Length.CompareTo(b.Length);

hello();
show("stock", 3);
Console.WriteLine(seven());
Console.WriteLine(add(2, 3));
Console.WriteLine(isEven(4));
Console.WriteLine(byLength("ab", "abc"));

実行結果:

hello
stock: 3
7
5
True
-1

デリゲートの変数は、メソッドと同じように 変数名(引数) で呼び出せます。 hello.Invoke() と書いても同じです。

ラムダ式の書き方の早見表

形 書き方
引数なし () => 7
引数1個(かっこを省略できる) n => n * 2
引数2個以上 (a, b) => a + b
型を明示する (int a, int b) => a + b
複数の文を書く (a, b) => { var s = a + b; return s; }
使わない引数を捨てる (_, e) => Console.WriteLine(e)
既定値を付ける(C# 12 以降) (int x, int step = 1) => x + step
非同期 async x => { await Task.Delay(10); return x; }
キャプチャを禁止する(C# 9 以降) static x => x * 2

標準ライブラリでの使われ方

Predicate<int> isEven = n => n % 2 == 0;
Comparison<string> byLength = (a, b) => a.Length.CompareTo(b.Length);

var nums = new List<int> { 5, 12, 8, 3, 20 };

Console.WriteLine(nums.Find(n => n > 6));                                    // Predicate<int>
Console.WriteLine(string.Join(",", nums.FindAll(isEven)));                   // Predicate<int>
Console.WriteLine(string.Join(",", nums.Where(n => n > 6).Select(n => n * 2))); // Func<int, bool> と Func<int, int>

var words = new List<string> { "pear", "fig", "banana" };
words.Sort(byLength);                                                        // Comparison<string>
Console.WriteLine(string.Join(",", words));

nums.ForEach(n => Console.Write(n + " "));                                   // Action<int>
Console.WriteLine();
Console.WriteLine(nums.Exists(n => n > 15) + " " + nums.TrueForAll(n => n > 0) + " " + nums.RemoveAll(n => n < 5));
12
12,8,20
24,16,40
fig,pear,banana
5 12 8 3 20 
True True 1
メソッド 引数の型 役割
List<T>.Find / FindAll / Exists / RemoveAll / TrueForAll Predicate<T> 条件に合うかを判定
List<T>.Sort Comparison<T> 2つの大小を比較
List<T>.ForEach Action<T> 各要素に処理を実行
LINQ の Where / Any / First Func<T, bool> 条件に合うかを判定
LINQ の Select / OrderBy Func<T, TResult> 値を変換、並べ替えのキーを取り出す

ラムダ式を直接渡す場合は、Predicate か Func かを意識しなくても動きます。 コンパイラが、引数の型に合わせてラムダ式を変換してくれるためです。

自作delegateからFunc、ラムダ式への書き換え

同じ「2つの整数を足す」処理を、C# 1.0 の書き方から順に短くしていきます。

Calc c1 = new Calc(Helpers.Add);                        // 1) 自作デリゲート+new(C# 1.0)
Calc c2 = Helpers.Add;                                  // 2) new を省略(C# 2.0)
Calc c3 = delegate (int a, int b) { return a + b; };    // 3) 匿名メソッド(C# 2.0)
Func<int, int, int> c4 = Helpers.Add;                   // 4) 自作の型を Func に置き換え
Func<int, int, int> c5 = (int a, int b) => { return a + b; };  // 5) ラムダ式(C# 3.0)
Func<int, int, int> c6 = (a, b) => a + b;               // 6) 型と return を省略
var c7 = (int a, int b) => a + b;                       // 7) 自然型で var(C# 10)

Console.WriteLine($"{c1(1,2)} {c2(1,2)} {c3(1,2)} {c4(1,2)} {c5(1,2)} {c6(1,2)} {c7(1,2)}");
Console.WriteLine(c7.GetType());
Console.WriteLine(c1.GetType());

delegate int Calc(int a, int b);
static class Helpers { public static int Add(int a, int b) => a + b; }
3 3 3 3 3 3 3
System.Func`3[System.Int32,System.Int32,System.Int32]
Calc

7通りとも同じ結果です。c7 の型は Func<int, int, int> になっています(実行時の表示では Func の後ろに型引数の数が付きます)。

flowchart LR
    A[1 自作 delegate + new] --> B[2 メソッド名だけ]
    B --> C[3 匿名メソッド]
    C --> D[4 Func に置き換え]
    D --> E[5 ラムダ式]
    E --> F[6 型と return を省略]
    F --> G[7 var と自然型]
段階 減ったもの
1 → 2 new Calc(...)
2 → 3 別の場所に書いたメソッド
3 → 4 自作のデリゲート型の宣言
4 → 5 delegate キーワード
5 → 6 引数の型、波かっこ、return
6 → 7 左辺の型名

自作のデリゲート型が向いている場面

ふだんは Func・Action で足ります。 自作のデリゲート型を宣言するとよいのは、次のような場面です。

場面 理由
引数の意味を名前で伝えたい Func<string, int, bool> より delegate bool StockCheck(string itemName, int required); のほうが、引数名が IntelliSense に出る
ref・out・in の引数を使う Func・Action の型引数では表現できない
同じ形のデリゲートを何か所も使う公開 API 型に名前があると、役割が伝わりやすい

PredicateとFuncは別の型

Predicate<int> と Func<int, bool> は形が同じですが、別の型なので、変数どうしの代入はできません。

Predicate<int> p1 = n => n > 0;
Func<int, bool> p2 = p1;
error CS0029: Cannot implicitly convert type 'System.Predicate<int>' to 'System.Func<int, bool>'

変換したいときは、new で包み直します。

Predicate<int> p1 = n => n > 0;
Func<int, bool> p2 = new Func<int, bool>(p1);
Console.WriteLine(p2(1));                                    // True

Func<int, bool> p3 = n => n > 0;
Console.WriteLine(nums.FindAll(new Predicate<int>(p3)).Count);

自分でメソッドを作るときは、引数の型を Func<T, bool> にしておくと、LINQ とそのまま組み合わせられて便利です。

C# 10の自然型:varでラムダ式を受ける

C# 10 から、ラムダ式やメソッドグループを var で受けられるようになりました。 コンパイラが、合う Func か Action を選んでくれます(これをラムダ式の「自然型」と呼びます)。Microsoft Learn のラムダ式のページを今回開いて、自然型・明示的な戻り値の型・static ラムダ・既定値つき引数の説明を確認しました。

var parse  = (string s) => int.Parse(s);
var log    = (string s) => Console.WriteLine(s);
var choose = object (bool b) => b ? 1 : "two";      // 戻り値の型を明示
var read   = Console.ReadLine;                      // メソッドグループ
var inc    = (int x, int step = 1) => x + step;     // 既定値つき(C# 12)
Delegate d = (int x) => x * 2;
object o   = () => "obj";

Console.WriteLine(parse.GetType());
Console.WriteLine(log.GetType());
Console.WriteLine(choose.GetType());
Console.WriteLine(read.GetType());
Console.WriteLine(inc.GetType());
Console.WriteLine(d.GetType());
Console.WriteLine(o.GetType());
Console.WriteLine(inc(5) + " " + inc(5, 3));
Console.WriteLine(d.DynamicInvoke(21));
System.Func`2[System.String,System.Int32]
System.Action`1[System.String]
System.Func`2[System.Boolean,System.Object]
System.Func`1[System.String]
<>f__AnonymousDelegate0`3[System.Int32,System.Int32,System.Int32]
System.Func`2[System.Int32,System.Int32]
System.Func`1[System.String]
6 8
42
書き方 推論された型
(string s) => int.Parse(s) Func<string, int>
(string s) => Console.WriteLine(s) Action<string>
object (bool b) => b ? 1 : "two" Func<bool, object>
Console.ReadLine Func<string>
(int x, int step = 1) => x + step コンパイラが生成した専用のデリゲート型

既定値つきの引数は Func では表現できないため、コンパイラが専用の型を作ります。 そのため inc(5) と引数を省略して呼び出せます。

自然型が決まらないとき

引数の型が書かれていないと、コンパイラは型を決められません。

var f1 = x => x * 2;
var f2 = (bool b) => b ? 1 : "two";
error CS8917: The delegate type could not be inferred.
エラーになる例 直し方
var f = x => x * 2; 引数に型を書く:var f = (int x) => x * 2;
var f = (bool b) => b ? 1 : "two"; 戻り値の型を書く:var f = object (bool b) => ...;
どちらも 左辺に型を書く:Func<int, int> f = x => x * 2;

なお、Microsoft Learn には「C# 14 から、ref などの修飾子を付けた引数でも型を省略できる」と記載されています。手元は C# 12 のため、この点は未実行です。

用途別のコピペ用サンプル

コールバック:進み具合と完了を呼び出し元に伝える

void Download(string name, Action<int> onProgress, Action<string> onDone)
{
    for (int p = 50; p <= 100; p += 50) onProgress(p);
    onDone(name);
}

Download("report.csv",
    p => Console.WriteLine($"{p}%"),
    n => Console.WriteLine($"saved {n}"));
50%
100%
saved report.csv

「いつ呼ぶか」は Download 側が決め、「呼ばれたら何をするか」は呼び出し側が決める、という分担になります。

処理の差し替え:辞書にFuncを入れる

switch で分岐していた計算を、名前と Func の対応表に置き換える例です。

var discounts = new Dictionary<string, Func<decimal, decimal>>
{
    ["none"]   = p => p,
    ["member"] = p => p * 0.9m,
    ["sale"]   = p => Math.Max(0, p - 500),
};

foreach (var k in discounts.Keys)
    Console.WriteLine($"{k}: {discounts[k](3000m)}");
none: 3000
member: 2700.0
sale: 2500

割引の種類が増えても、辞書に1行足すだけで対応できます。

リトライの共通化:Funcで「やり直す処理」を受け取る

int calls = 0;
int r = Retry.Run(
    () => { calls++; if (calls < 3) throw new InvalidOperationException("busy"); return 42; },
    maxAttempts: 5,
    onRetry: (n, ex) => Console.WriteLine($"retry {n}: {ex.Message}"));
Console.WriteLine($"result={r} calls={calls}");

try
{
    Retry.Run<int>(() => throw new TimeoutException("timeout"), 2,
        (n, ex) => Console.WriteLine($"retry {n}: {ex.Message}"));
}
catch (Exception ex) { Console.WriteLine("gave up: " + ex.GetType().Name); }

static class Retry
{
    public static T Run<T>(Func<T> action, int maxAttempts, Action<int, Exception>? onRetry = null)
    {
        for (int attempt = 1; ; attempt++)
        {
            try { return action(); }
            catch (Exception ex) when (attempt < maxAttempts) { onRetry?.Invoke(attempt, ex); }
        }
    }
}
retry 1: busy
retry 2: busy
result=42 calls=3
retry 1: timeout
gave up: TimeoutException

Func<T> で「やり直したい処理」を、Action<int, Exception> で「やり直すときの通知」を受け取っています。 最後の試行で例外が出た場合は、when の条件が false になるので、例外がそのまま呼び出し元へ届きます。onRetry?.Invoke(...) は、null のときは何もしない書き方です。

遅延実行:必要になるまで計算しない

Func<string> heavy = () => { Console.WriteLine("  (computing)"); return "value"; };

void LogDebug(bool enabled, Func<string> message)
{
    if (enabled) Console.WriteLine(message());
}

LogDebug(false, heavy);
Console.WriteLine("skipped");
LogDebug(true, heavy);

var lazy = new Lazy<string>(heavy);
Console.WriteLine(lazy.IsValueCreated);
Console.WriteLine(lazy.Value);
Console.WriteLine(lazy.Value);
skipped
  (computing)
value
False
  (computing)
value
value

enabled が false のときは heavy が呼ばれていません。値そのものではなく「値の作り方」を Func で渡すと、使うときまで計算を先送りできます。 Lazy<T> は、最初に Value を読んだときに1回だけ計算し、2回目以降は保存した値を返します。

LINQ も同じ仕組みです。Where に渡したラムダ式は、結果を取り出すときに初めて呼ばれます。

var nums = new List<int> { 5, 12, 8, 20 };
var q = nums.Where(n => { Console.WriteLine($"  check {n}"); return n > 10; });
Console.WriteLine("query built");
Console.WriteLine(q.First());
query built
  check 5
  check 12
12

前後の処理を共通化する

T Measure<T>(string label, Func<T> body)
{
    try { return body(); }
    finally { Console.WriteLine($"{label} done"); }
}

Console.WriteLine(Measure("sum", () => Enumerable.Range(1, 100).Sum()));
sum done
5050

ログ出力、時間の計測、後片付けなど、「本体の前後で必ず行う処理」を1か所にまとめられます。

関数を組み合わせる・関数を返す

Func<int, int> twice = x => x * 2;
Func<int, int> plus1 = x => x + 1;
Func<int, int> Compose(Func<int, int> f, Func<int, int> g) => x => g(f(x));
Console.WriteLine(Compose(twice, plus1)(5));

Func<int, Func<int, int>> adder = a => b => a + b;
Console.WriteLine(adder(3)(4));
11
7

複数のメソッドを登録する:マルチキャスト

デリゲートは += で複数のメソッドを登録でき、呼び出すと登録順に実行されます。

Action<string> notify = m => Console.WriteLine("mail: " + m);
notify += m => Console.WriteLine("log : " + m);
Action<string> chat = m => Console.WriteLine("chat: " + m);
notify += chat;
notify("done");

notify -= chat;
Console.WriteLine(notify.GetInvocationList().Length);

Func<int> multi = () => 1;
multi += () => 2;
Console.WriteLine(multi());

Action<string>? none = null;
none?.Invoke("x");
Console.WriteLine("null ok");
mail: done
log : done
chat: done
2
2
null ok
  • -= で外すには、登録したときと同じデリゲートが必要です。 ラムダ式を直接 += した場合は、同じ式をもう一度書いても外せないので、後で外すものは chat のように変数へ入れておきます。
  • Func を複数登録すると、受け取れる戻り値は最後の1つだけです(上の例では 2)。複数登録は、戻り値のない Action やイベントで使うのが基本です。
  • 中身が null のデリゲートを呼ぶと例外になるので、null の可能性があるときは ?.Invoke(...) を使います。

イベントとEventHandler

イベントは、「外からは += と -= だけができる」ように保護されたデリゲートです。 発行(呼び出し)できるのは、宣言したクラスの中だけになります。

var st = new Stock();
st.Changed += (sender, e) => Console.WriteLine($"{e.Name}: {e.Before} -> {e.After}");

EventHandler<StockChangedEventArgs> low = (s, e) => { if (e.After < 3) Console.WriteLine("low stock!"); };
st.Changed += low;

st.Set("pen", 10);
st.Set("pen", 2);

st.Changed -= low;
st.Set("pen", 1);

class StockChangedEventArgs : EventArgs
{
    public required string Name { get; init; }
    public int Before { get; init; }
    public int After { get; init; }
}

class Stock
{
    private readonly Dictionary<string, int> _items = new();
    public event EventHandler<StockChangedEventArgs>? Changed;

    public void Set(string name, int count)
    {
        _items.TryGetValue(name, out var before);
        _items[name] = count;
        Changed?.Invoke(this, new StockChangedEventArgs { Name = name, Before = before, After = count });
    }
}
pen: 0 -> 10
pen: 10 -> 2
low stock!
pen: 2 -> 1

3回目の Set では low を外しているので、「low stock!」は出ていません。

観点 デリゲート型のフィールド・プロパティ event
外から += / -= できる できる
外から = で上書き できる できない
外から呼び出す できる できない
向いている用途 1つの処理を受け取るコールバック 複数の購読者に知らせる通知

EventHandlerとActionのどちらでイベントを宣言するか

public event Action<int>? Simple; のように、Action でイベントを宣言することもできます(手元でも動作しました)。

宣言 特徴
event EventHandler<TEventArgs> .NET の標準的な形。送信元(sender)が分かり、後からイベントデータにプロパティを足しても購読側の引数が変わらない
event Action<T> 短く書ける。クラスの内部や小さなツールで手軽に使える

ライブラリとして公開するクラスや、UI フレームワークと組み合わせるクラスでは EventHandler<TEventArgs> にしておくと、ほかの .NET のコードと形がそろいます。

クロージャ:外側の変数を使うラムダ式

ラムダ式は、外側にあるローカル変数を読み書きできます。これを「キャプチャ」と呼び、変数をキャプチャしたラムダ式を「クロージャ」と呼びます。

int counter = 0;
Action up = () => counter++;
up(); up();
Console.WriteLine(counter);

Func<int> MakeCounter() { int n = 0; return () => ++n; }
var ca = MakeCounter();
var cb = MakeCounter();
Console.WriteLine($"{ca()} {ca()} {ca()} / {cb()}");
2
1 2 3 / 1

キャプチャされるのは「値のコピー」ではなく「変数そのもの」です。 そのため、ラムダ式の中での変更が外側にも反映され、MakeCounter を抜けた後も n が生き続けます。ca と cb は別々の n を持っています。

ループ変数をキャプチャしたときの挙動

var fs = new List<Func<int>>();

for (int i = 0; i < 3; i++) fs.Add(() => i);
Console.WriteLine("for    : " + string.Join(",", fs.Select(f => f())));

fs.Clear();
for (int i = 0; i < 3; i++) { int copy = i; fs.Add(() => copy); }
Console.WriteLine("copy   : " + string.Join(",", fs.Select(f => f())));

fs.Clear();
foreach (var i in new[] { 0, 1, 2 }) fs.Add(() => i);
Console.WriteLine("foreach: " + string.Join(",", fs.Select(f => f())));
for    : 3,3,3
copy   : 0,1,2
foreach: 0,1,2
ループ 結果 理由
for の変数をそのまま使う 3,3,3 変数 i はループ全体で1つ。呼び出した時点の値(ループ終了後の 3)が読まれる
ループの中でコピーする 0,1,2 copy は周回ごとに新しい変数
foreach の変数 0,1,2 foreach の変数は周回ごとに新しい変数として扱われる

for の中でラムダ式を作って後から呼ぶときは、ループの中で別の変数にコピーしてから使います。

flowchart LR
    subgraph forループ
      I[変数 i は1つ] --> L1[ラムダ1]
      I --> L2[ラムダ2]
      I --> L3[ラムダ3]
    end
    subgraph コピーした場合
      C1[copy = 0] --> M1[ラムダ1]
      C2[copy = 1] --> M2[ラムダ2]
      C3[copy = 2] --> M3[ラムダ3]
    end

staticラムダでキャプチャを防ぐ

C# 9 から、ラムダ式に static を付けると、外側のローカル変数やインスタンスのメンバーをキャプチャしないことをコンパイラが保証してくれます。

const int Rate = 10;
Func<int, int> tax = static p => p * Rate / 100;   // 定数は使える
Console.WriteLine(tax(1200));
120

ローカル変数を使うとコンパイルエラーになります。

int rate = 10;
Func<int, int> tax = static p => p * rate / 100;
error CS8820: A static anonymous function cannot contain a reference to 'rate'.
観点 ふつうのラムダ式 static ラムダ
外側のローカル変数 使える 使えない(コンパイルエラー)
定数・static メンバー 使える 使える
向いている場面 外側の状態を使いたいとき 引数だけで完結する処理、何度も呼ばれる箇所

キャプチャがあると、変数を保持するためのオブジェクトが作られます。引数だけで完結するラムダ式に static を付けておくと、意図しないキャプチャに気づけるので、LINQ の中やループの中で使う処理におすすめです。必要な値は引数として渡します。

ref・out引数はキャプチャできない

void M(ref int v) { Action z = () => v++; }
error CS1628: Cannot use ref, out, or in parameter 'v' inside an anonymous method, lambda expression, query expression, or local function

いったんローカル変数にコピーしてから使うと解決します。

非同期処理ではTaskを返すFuncを使う

async のラムダ式は、Func<Task>(戻り値なし)か Func<Task<T>>(戻り値あり)で受けます。

Func<Task> wait = async () => { await Task.Delay(10); Console.WriteLine("waited"); };
Func<int, Task<int>> twice = async x => { await Task.Delay(10); return x * 2; };

await wait();
Console.WriteLine(await twice(21));

var af = async (int x) => { await Task.Delay(1); return x + 1; };
Console.WriteLine(af.GetType());
waited
42
System.Func`2[System.Int32,System.Threading.Tasks.Task`1[System.Int32]]

var で受けた af も、Func<int, Task<int>> と推論されています。

同期の型 非同期の型 ラムダ式の例
Action Func<Task> async () => { await ...; }
Action<T> Func<T, Task> async x => { await ...; }
Func<TResult> Func<Task<TResult>> async () => { await ...; return v; }
Func<T, TResult> Func<T, Task<TResult>> async x => { await ...; return v; }

asyncのラムダ式をActionで受けるとどうなるか

Action asyncVoid = async () => { await Task.Delay(1); };
Console.WriteLine(asyncVoid.Method.ReturnType);

Func<Task> bad = async () => { await Task.Delay(1); throw new InvalidOperationException("in Func<Task>"); };
try { await bad(); }
catch (Exception ex) { Console.WriteLine("caught: " + ex.Message); }
System.Void
caught: in Func<Task>

Action で受けると、戻り値が void のメソッド(async void)になります。Task が返らないので、呼び出し側は完了を待つことも、例外を try-catch で受け取ることもできません。 Func<Task> で受ければ、await で完了を待てて、例外も捕まえられます。

自作のメソッドで非同期のコールバックを受け取るときは、引数の型を Func<Task> にしておくと、呼び出し側が async のラムダ式を渡したときに自動で Task を返す形になります。

非同期版のリトライ

int ac = 0;
int ar = await RetryAsync.RunAsync(async () =>
{
    ac++;
    await Task.Delay(5);
    if (ac < 2) throw new IOException("net");
    return 7;
}, maxAttempts: 3);
Console.WriteLine($"async result={ar} calls={ac}");

static class RetryAsync
{
    public static async Task<T> RunAsync<T>(Func<Task<T>> action, int maxAttempts, int delayMs = 10)
    {
        for (int attempt = 1; ; attempt++)
        {
            try { return await action(); }
            catch (Exception) when (attempt < maxAttempts) { await Task.Delay(delayMs * attempt); }
        }
    }
}
async result=7 calls=2

複数の非同期処理をまとめて待つ

var results = await Task.WhenAll(new[] { 1, 2, 3 }.Select(async n => { await Task.Delay(5); return n * n; }));
Console.WriteLine(string.Join(",", results));
1,4,9

Select に async のラムダ式を渡すと Task の並びになるので、Task.WhenAll でまとめて待ちます。

Expressionで包んだFuncとの違い

Func<T> は「実行できるコード」、Expression<Func<T>> は「コードの構造を表すデータ(式ツリー)」です。 同じラムダ式でも、代入先の型によってコンパイル結果が変わります。

using System.Linq.Expressions;

Func<int, bool> f1 = n => n > 10;
Expression<Func<int, bool>> e1 = n => n > 10;

Console.WriteLine(f1);
Console.WriteLine(e1);
Console.WriteLine(e1.Body.NodeType + " " + ((BinaryExpression)e1.Body).Right);
Console.WriteLine(e1.Compile()(11));

Expression<Func<Item, string>> prop = x => x.Name;
Console.WriteLine(((MemberExpression)prop.Body).Member.Name);

record Item(string Name);
System.Func`2[System.Int32,System.Boolean]
n => (n > 10)
GreaterThan 10
True
Name

f1 は中身を表示できませんが、e1 は「n が 10 より大きい」という構造をデータとして持っていて、演算子の種類(GreaterThan)や右辺の値(10)を取り出せます。

観点 Func<T, bool> Expression<Func<T, bool>>
中身 コンパイル済みの実行コード 式の構造を表すデータ
実行 そのまま呼べる Compile() してから呼ぶ
中身の解析 できない できる(演算子・プロパティ名などを取り出せる)
使われる場所 IEnumerable<T> の LINQ(メモリ上の処理) IQueryable<T> の LINQ(式を別の形に変換して実行する仕組み)
書ける内容 何でも 式1つ(波かっこの本体は書けない)
Expression<Func<int, int>> e = n => { return n + 1; };
error CS0834: A lambda expression with a statement body cannot be converted to an expression tree

IQueryableにFuncを渡すとメモリ上の処理になる

Func<int, bool> f1 = n => n > 10;
Expression<Func<int, bool>> e1 = n => n > 10;
var list = new List<int> { 5, 20 };

Console.WriteLine(list.AsQueryable().Where(e1).Expression);
Console.WriteLine(list.AsQueryable().Where(f1).GetType().Name);
System.Collections.Generic.List`1[System.Int32].Where(n => (n > 10))
WhereEnumerableIterator`1

式ツリーを渡した1行目は、条件が「式」としてクエリに組み込まれています。Func を渡した2行目は、IEnumerable<T> 用の Where が選ばれ、メモリ上で1件ずつ判定する処理になりました。

データベースに問い合わせるライブラリの多くは、IQueryable<T> に渡された式ツリーを SQL に変換します。検索条件を変数に入れて使い回すときは、変数の型を Expression<Func<T, bool>> にしておくと、条件が式のままクエリへ渡ります(データベースへ接続しての確認は未実行で、上の AsQueryable の結果から分かる範囲の説明です)。

型を選ぶための判断表

やりたいこと 選ぶ型
処理を渡したい。戻り値は不要 Action / Action<T>
処理を渡したい。戻り値がほしい Func<TResult> / Func<T, TResult>
条件を渡したい Func<T, bool>(List<T> のメソッドに合わせるなら Predicate<T>)
並び順を渡したい Comparison<T>、または OrderBy にキーを取り出す Func<T, TKey>
非同期の処理を渡したい Func<Task> / Func<Task<TResult>>
複数の相手に通知したい event EventHandler<TEventArgs>
条件をデータベースの問い合わせに使いたい Expression<Func<T, bool>>
ref・out の引数がある、引数に名前を付けたい 自作のデリゲート型
その場でだけ使う小さな関数 var f = (int x) => ...; かローカル関数

ローカル関数との使い分け

メソッドの中にメソッドを書く「ローカル関数」も、Func の変数に入れられます。

int Triple(int x) => x * 3;
Func<int, int> lf = Triple;
Console.WriteLine(lf(3));   // 9
観点 ラムダ式 ローカル関数
引数として渡す 得意(その場で書ける) 名前で渡せる
再帰・yield return・ジェネリック 書きにくい 書ける
名前 なし あり(スタックトレースで見つけやすい)

ほかのメソッドに渡すだけならラムダ式、同じメソッドの中で何度も呼ぶ補助的な処理ならローカル関数、という分け方が分かりやすいでしょう。

つまずきやすいエラーと直し方

エラー 起きる場面 直し方
CS8917: The delegate type could not be inferred. var f = x => x * 2; 引数の型か、左辺の型を書く
CS0029: Cannot implicitly convert type 'System.Predicate<int>' to 'System.Func<int, bool>' Predicate の変数を Func に代入 new Func<int, bool>(p) で包む
CS0029: Cannot implicitly convert type 'bool' to 'int'(CS1662 も出る) list.Sort((a, b) => a > b) a.CompareTo(b) で int を返す
CS1643: Not all code paths return a value in lambda expression of type 'Func<int>' Func なのに return がない 値を返すか、Action に変える
CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement Action a = () => 5; 値を返すなら Func に変える
CS8820: A static anonymous function cannot contain a reference to 'rate'. static ラムダでローカル変数を使用 引数で渡すか、static を外す
CS1628: Cannot use ref, out, or in parameter ... ラムダ式の中で ref 引数を使用 ローカル変数にコピーしてから使う
CS0834: A lambda expression with a statement body cannot be converted to an expression tree 式ツリーに波かっこの本体 式1つで書く

(いずれも .NET SDK 8.0.131 で実際に出力されたメッセージです。)

よくある質問

Q. Func と Action はどう使い分けますか? A. 戻り値が必要なら Func、不要なら Action です。Func は型引数の最後が戻り値の型になります。非同期の処理を受け取る場合は、どちらの場合も Task を返す Func(Func<Task> や Func<Task<T>>)を使います。

Q. デリゲートとラムダ式の違いは何ですか? A. デリゲートは「メソッドを入れておける型」、ラムダ式は「名前のないメソッドをその場で書く書き方」です。ラムダ式で書いた処理を、デリゲート型(Func や Action など)の変数に入れて使います。

Q. Predicate と、bool を返す Func はどちらを使えばよいですか? A. 自分で作るメソッドの引数は、LINQ と形がそろう Func<T, bool> がおすすめです。List<T>.Find などは Predicate を受け取りますが、ラムダ式を直接渡す場合はどちらでも同じ書き方で動きます。

Q. ラムダ式を -= で外せないのはなぜですか? A. 同じ内容のラムダ式を2回書いても、別々のデリゲートになるためです。後で外す予定のものは、いったん変数に入れてから += し、同じ変数で -= します。

まとめ

  • Func は戻り値あり、Action は戻り値なしの汎用デリゲート型。Func の型引数は最後の1つが戻り値。
  • デリゲートは型、ラムダ式は書き方。ラムダ式で書いた処理を Func・Action の変数に入れて使う。
  • Predicate・Comparison・EventHandler は用途が決まった型で、List<T> のメソッドやイベントで使われる。
  • C# 10 以降は var f = (int x) => x * 2; と書ける。引数の型が決まらないときは CS8917 になるので、型を書き足す。
  • for の変数をキャプチャするときは、ループ内でコピーする。引数だけで完結する処理には static を付けると意図しないキャプチャを防げる。
  • 非同期のコールバックは Func<Task> で受けると、完了を待てて例外も受け取れる。
  • Expression<Func<T>> は式の構造を表すデータ。IQueryable<T> に条件を渡すときはこちらを使う。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# FuncとActionの使い方 デリゲート・ラムダ式との違いを図解」

株式会社RJCでは、C#のデリゲートやラムダ式の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?