内置委托 Func、Action 与 Predicate
本教程共 100 篇 · 第 55 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
55. 内置委托 Func、Action 与 Predicate
本节目标:学完你能用三个内置泛型委托代替自定义 delegate,少声明类型、专注逻辑。
上一章你用 delegate 自己定义类型。但你会发现,绝大多委托都长这样:无返回值、或带返回值、或做个判断。.NET 早就准备了三种泛型委托,覆盖九成场景,不用再手写。
把这三个内置委托想成「三种标准插座」:Action 是「只干活不回报」的插座,Func 是「干完活带个结果回来」的插座,Predicate 是「判断真假」的专用插座。有了标准插座,电器(方法)插上就能用,不必每家自备一种接口。
一、Action:无返回值
Action 代表「有参数、没返回值」的方法。参数个数从 0 到 16:
namespace CSharpDemo;
Action<string> shout = s => Console.WriteLine(s.ToUpper());
shout("hi"); // HI
Action<int, int> add = (a, b) => Console.WriteLine(a + b);
add(3, 4); // 7
没有参数时用 Action(无泛型参数);有返回值就轮到 Func。
Action 没有返回值,所以它适合「做一件事」而非「求一个结果」:记日志、发通知、刷新界面都算。一旦你需要把算出来的东西带回来交给调用方,就换 Func。简单记:Action 管「动手」,Func 管「动手还交差」。
二、Func:有返回值
Func 最后一个类型参数是返回值类型,前面的是入参:
namespace CSharpDemo;
Func<int, int, int> multiply = (a, b) => a * b;
Console.WriteLine(multiply(3, 4)); // 12
Func<string, int> length = s => s.Length;
Console.WriteLine(length("apple")); // 5
Note记不住顺序就看最后一项:
Func<入参..., 返回值>。返回值永远在最右。这是约定,照着用就不会错。
三、Predicate:返回 bool 的判断
Predicate<T> 专门表示「对一个 T 做判断,返回 true/false」:
namespace CSharpDemo;
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(7)); // False
// Func<int, bool> f = isEven; // 错误:两种类型不能互换,结构等价≠类型等价
它等价于 Func<int, bool> 吗?结构上等价,但类型上不等价,不能互相赋值。
Tip这点是新手常踩的雷。C# 看的是类型名字,不是结构。所以别指望把
Predicate当Func用,或反过来。需要判断时,按 API 要求的类型来。
四、三者一览对照表
把三个放一起看更清楚:
| 内置委托 | 入参 | 返回值 | 典型用途 |
|---|---|---|---|
Action | 0–16 个 | 无 | 执行一个动作、打印、通知 |
Func<T1,…,TResult> | 0–16 个 | 有(最后一个类型参数) | 计算、转换、取值 |
Predicate<T> | 1 个(T) | bool | 判断、筛选、匹配 |
Predicate<T> 其实是 Func<T, bool> 的「专用别名」。API 用 Predicate 时语义更明确(「这是个判断」),但底层能力和 Func<T,bool> 一样。
五、为什么还要它们
自定义 delegate 要写两行声明,内置委托一行搞定。更重要的好处是统一:整个生态都用 Action/Func,方法签名彼此对得上,复用度高。
namespace CSharpDemo;
int Square(int x) => x * x;
Func<int, int> sq = Square;
Console.WriteLine(sq(5)); // 25
Note参数多的极端场景(超过 16 个),内置委托不够用,才回去自定义
delegate。日常几乎遇不到。
六、实战中的典型用法
筛选列表就是 Predicate / Func 的舞台。下面用 Func 当判断条件:
namespace CSharpDemo;
List<int> numbers = [1, 2, 3, 4, 5, 6];
var evens = numbers.FindAll(n => n % 2 == 0);
foreach (var n in evens) Console.WriteLine(n); // 2 4 6
FindAll 接受 Predicate<int>,而 Lambda n => n % 2 == 0 既能转成 Predicate 也能转成 Func,这里正好匹配。
Action 常用于「对每个元素做件事」:
namespace CSharpDemo;
List<string> names = ["小明", "小红"];
names.ForEach(n => Console.WriteLine($"你好,{n}"));
把委托当方法参数
内置委托最常见的用法是「作为方法的参数」,把行为传进去:
namespace CSharpDemo;
void Repeat(int times, Action action)
{
for (int i = 0; i < times; i++) action();
}
Repeat(3, () => Console.WriteLine("喵"));
// 输出三行:喵
这样 Repeat 不关心「重复时干什么」,只管重复,行为由调用方决定。
这种「把行为注入方法」的写法看似简单,却是很多框架的底层套路:测试时你注入一个打印到字符串的 Action,生产时换成写文件的 Action,同一个 Repeat 一行都不用改。委托让「相同的流程、不同的落地」变得轻而易举,这也是依赖注入(dependency injection)思想的雏形——把「做什么」从「怎么做」里解耦出来。
七、把 Func 当返回值
委托不仅能当参数,还能当返回值——返回一个「还没执行的计算」,调用方决定何时跑:
namespace CSharpDemo;
Func<int, int> MakeMultiplier(int factor) =>
x => x * factor;
var doubleIt = MakeMultiplier(2);
var tripleIt = MakeMultiplier(3);
Console.WriteLine(doubleIt(5)); // 10
Console.WriteLine(tripleIt(5)); // 15
MakeMultiplier 返回了一个「记住了 factor 的 Lambda」,这就是闭包在委托上的应用。它常用来生成一批相似但参数不同的处理逻辑。
八、和自定义委托怎么选
优先用内置委托。除非你需要给委托一个有意义的名字(比如 delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice)),让读代码的人一眼懂意图,才自定义。名字本身就是文档。
Tip给委托起好名字,是 API 设计的小技巧。事件相关的委托尤其该如此,后面讲事件模式时会看到。
九、常见误用
误用一:把 Predicate<T> 当 Func<T,bool> 传。两者类型不同,API 要哪个就得给哪个;靠 Lambda 推导通常能自动匹配,但显式声明变量时别强行互转。
误用二:Func 的返回值是最后一个类型参数,新手常把入参个数和返回值顺序写反,导致编译报「参数不匹配」。记住「返回值永远在右」。
误用三:在热路径上反复 new 委托(如循环里写 list.ForEach(x => ...) 每次创建委托)。单次无妨,极高频时要留意分配,可把 Lambda 提到循环外复用。
十、最佳实践
- 普通回调、传行为,优先
Action/Func,别急着自定义delegate。 - 记住
Func最后一个类型是返回值,别把入参和返回值顺序搞反。 - 需要「判断」时,按 API 要求的类型来(可能是
Predicate或Func<T,bool>),别混用。 - 超过 16 个参数才考虑自定义委托,日常用内置足够。
- 把委托当参数时,命名参数 + 简明 Lambda,让调用处一眼可读。
十一、综合运用小例
把三者合起来:Action 负责「做」、Func 负责「算」、Predicate 负责「判」,写一条清晰的小流水线:
namespace CSharpDemo;
List<int> nums = [1, 2, 3, 4, 5, 6, 7, 8];
Predicate<int> isOdd = n => n % 2 == 1; // 判:是不是奇数
Func<int, int> square = n => n * n; // 算:平方
Action<int> show = n => Console.WriteLine($"平方={n}"); // 做:打印
foreach (var n in nums.FindAll(isOdd))
show(square(n));
// 输出:平方=1 平方=9 平方=25 平方=49
这段代码的妙处在于「筛选—计算—展示」三件事分别交给了三种恰当的委托,主流程读起来像在说人话:FindAll(isOdd) 取奇数,对每个数先 square 再 show。当你熟悉了这套分工,Action/Func/Predicate 会成为你写 LINQ 与回调时的日常词汇。
Tip一开始不必强求都用委托。先从「哪里要传一个方法当参数」想起,那里八成就是
Action或Func的用武之地。
内置委托为何能撑起整个生态
内置委托最容易被低估的能力是”延迟执行(deferred execution)“。把 Func 当返回值(如前面的 MakeMultiplier),你其实把一段计算”打包”交给别人,由对方决定何时、是否运行。这种”计算即数据”的思路,正是 LINQ 延迟执行、yield 迭代器的底层基石——查询定义了,却等到真正遍历才动手。
内置委托之所以能撑起整个 .NET 回调生态,关键在于它们”无名字却有形状”。Action/Func/Predicate 不关心方法叫什么,只关心”长什么样、返回什么”。正因如此,一个 List<T>.ForEach(Action<T>) 能接住你写的任何打印逻辑,一个 Where(Func<T,bool>) 能接住任何判断。类型统一了,复用才成为可能——这比每个项目各起一套委托名字要省心得多。
十二、小结
Action 管「做事不返回」,Func 管「算完返回」,最后一参是返回值;Predicate<T> 管「判断真假」,但和 Func<T,bool> 不能混用。三者取代绝大多数自定义 delegate,让代码更短、生态更统一。下一章用它们搭出事件模型。