首页 / C# 入门教程 / 内置委托 Func、Action 与 Predicate

C# 入门教程

内置委托 Func、Action 与 Predicate

本教程共 100 篇 · 第 55 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

C#C# 入门教程编程语言委托FuncActionPredicate

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# 看的是类型名字,不是结构。所以别指望把 PredicateFunc 用,或反过来。需要判断时,按 API 要求的类型来。

四、三者一览对照表

把三个放一起看更清楚:

内置委托入参返回值典型用途
Action0–16 个执行一个动作、打印、通知
Func<T1,…,TResult>0–16 个有(最后一个类型参数)计算、转换、取值
Predicate<T>1 个(Tbool判断、筛选、匹配

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 要求的类型来(可能是 PredicateFunc<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) 取奇数,对每个数先 squareshow。当你熟悉了这套分工,Action/Func/Predicate 会成为你写 LINQ 与回调时的日常词汇。

Tip

一开始不必强求都用委托。先从「哪里要传一个方法当参数」想起,那里八成就是 ActionFunc 的用武之地。

内置委托为何能撑起整个生态

内置委托最容易被低估的能力是”延迟执行(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,让代码更短、生态更统一。下一章用它们搭出事件模型。