Menu

C# Func 与 Action:泛型委托、Predicate 及示例

C# 中 Func 和 Action 如何工作:Func<T, TResult> 用于有返回值的函数,Action<T> 用于没有返回值的函数,Predicate 和 Comparison,把函数作为参数传入和作为返回值返回,命令字典,异步 lambda,以及什么时候应该自己声明委托。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Func 和 Action 是 .NET 为你预先声明好的泛型委托类型,所以你不必先声明委托类型,就能存储和传递函数。Func 有返回值;Action 什么也不返回。

输出:

4
46.00
False
Hello!
Mia scored 87

需要记住的规则:在 Func 中,最后一个类型参数是返回类型。Func<int, int, bool> 接收两个 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(...) 接收的 lambda 形状相同,类型却不同:

输出:

17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y

把函数传给方法

Func 或 Action 参数让调用方可以提供算法中变化的那一部分。两种常见的形式是“运行它并测量”和“反复尝试直到成功”:

输出:

attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2

Retry 是泛型方法,所以它的 Func<int, T> 可以返回任何类型;编译器根据 lambda 把 T 推断为 string。传给 Twice 的 lambda 修改了局部变量 clicks,这能生效是因为 lambda 捕获的是变量本身,而不是它的值的副本。

方法组:按名称传递方法

填充 Func 或 Action 不一定需要 lambda。签名匹配的现有方法的名称(称为“方法组”)可以直接转换:

输出:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) 和 Select(s => int.Parse(s)) 效果相同,只是少了一层调用。编译器会选择适合目标类型的重载:int.Parse 有多个重载,只有 Parse(string) 匹配 Func<string, int>。当编译器无法确定唯一的重载时(有多个重载同样合适,或者无法从方法组推断出泛型方法的类型参数),调用就无法编译,这时改用一个显式写出调用的 lambda 即可解决。

作为 Func 使用的方法组同样是在之后委托被调用时才运行,和 lambda 完全一样。区别只在于写法。

返回函数

方法可以构建并返回一个函数。返回的 Func 会带着构建它时用到的值:

输出:

140
180
162

Twice(staff) 构建了一个把员工折扣应用两次的新函数:200,然后 180,然后 162。

函数字典

把函数存进字典,就能把冗长的 switch 变成一张查找表。这是命令处理器、计算器和菜单操作的常用写法,而且可以在运行时添加新条目:

输出:

12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE

添加一个运算符只需在字典中多加一个条目,循环无需任何改动。传给第二个字典的 StringComparer.OrdinalIgnoreCase 让命令名不区分大小写。

异步函数:用 Func<Task>,而不是 Action

在异步代码中,委托类型决定了调用方能否等待这项工作完成:

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 的异步 lambda 会变成一个 async void 方法:没有任何东西能等待它,它内部的异常也永远不会到达调用方。异常会在线程池上(桌面应用中则是在 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 参数,泛型委托同样无法拥有这种参数。

事件是另一个仍会出现自定义委托类型的地方,不过内置的 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);。调用方传入一个 lambda(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 使用。这两种类型之间不能互相转换,但一个 lambda 可以转换成其中任意一种。

如何在异步代码中使用 Func?

没有结果的异步函数用 Func<Task>,有结果的用 Func<T, Task<TResult>>,这样调用方就可以 await 它。不要把异步 lambda 赋给 Action:那样它会变成 async void,既不能被等待,它的异常也无法被调用方捕获。

Func 可以有 out 或 ref 参数吗?

不可以。Func 和 Action 的泛型参数都是普通的值参数,所以无法写出 Func<string, out int, bool>。对于像 TryParse 这样的签名,请声明一个自定义委托:delegate bool TryParser<T>(string text, out T value);。

Coddy 编程语言插图

和 Coddy 一起学编程

开始