- C#
C# FuncとActionの使い方 デリゲート・ラムダ式との違いを図解
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期連続の黒字決算。