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> | 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(...) 接收的 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);。