c# 里面的 func<> 是什么?
C# Func<>
Func<TResult>是.NET 内置的泛型委托,用来封装有返回值的方法;与之对应:
Func:有返回值Action:无返回值(void)
委托:可以把方法当成变量来传递的类型。
重载版本
Func最多支持16 个输入参数,最后一个泛型参数永远是返回值类型。
// 无参数,返回 TResult public delegate TResult Func<TResult>(); // 1个输入参数T1,返回TResult public delegate TResult Func<T1, TResult>(T1 arg1); // 2个输入参数 public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2); // …一直到 Func<T1,…,T16,TResult>✅记忆:最后一个类型是返回值,前面全是入参。
简单示例
1. 普通方法赋值给 Func
// Func<int, int>:输入int,返回int Func<int, int> square = CalcSquare; int res = square(5); // 调用,res=25 int CalcSquare(int x) { return x * x; }2. 搭配 Lambda(最常用)
日常几乎都是配合 lambda 简写,不用单独写方法:
Func<int, int> square = x => x * x; Console.WriteLine(square(6)); //36 //两个入参,返回bool Func<int, int, bool> isBigger = (a, b) => a > b; Console.WriteLine(isBigger(10,2)); //True //无参数有返回值 Func<string> getMsg = () => "hello func"; Console.WriteLine(getMsg());和 Action 的对比
表格
| 类型 | 是否有返回值 | 示例 |
|---|---|---|
Func<TResult> | ✅有返回值 | Func<int> f=()=>10; |
Action | ❌void 无返回 | Action a=()=>Console.WriteLine(1); |
常见使用场景
- LINQ:Where、Select 接收的就是 Func 委托
var list = new List<int>{1,2,3,4}; // Where接收 Func<int,bool> var r = list.Where(x => x>2);- 回调函数、策略模式,把一段逻辑当参数传入方法
//接收一个Func做计算逻辑 static int DoWork(Func<int,int> op, int val) { return op(val); } //调用 int v = DoWork(x=>x*x,7);- 延迟执行:把逻辑包装起来,后面再执行
func()
容易踩坑点
- 最后一个泛型是返回值,不要写反
//错误!以为<int,int>是两个int入参,实际第一个int是返回值,没有入参 Func<int,int> bad; // ✔ 第一个int入参,第二个int返回 Func<int, int> ok;Func不能返回 void;要 void 就用Action
// Func<int,void> 编译报错!void不能做泛型参数 Action<int> act = x=>Console.WriteLine(x); //正确- 闭包陷阱:lambda 捕获外部变量,是捕获变量本身不是当时的值。
小结
Func<...>= 可以存带返回值方法的泛型委托,最后一个泛型参数代表返回类型,广泛用于 Lambda、LINQ、回调。
如果你需要,我可以给你对比Predicate、Func、Action三者区别。
相当于 delegate 的语法糖?
Func<> 是不是 delegate 的语法糖?
不完全是语法糖,它本身就是编译器预定义好的泛型 delegate 类型。
本质
public delegate TResult Func<TResult>(); public delegate TResult Func<T1,TResult>(T1 arg1);Func、Action就是.NET 类库里面已经帮你写好的 delegate 声明,不是 C# 编译器给你做的语法转换糖。
语法糖:编译器看到简写,自动帮你生成 delegate 代码,比如
=>lambda 是语法糖。 Func<>:是 BCL 已经定义好的委托类型,你直接拿来用。
对比
- 自己手写 delegate:
// 自己声明一个委托类型 public delegate int MyCalc(int x);- 使用内置 Func,效果逻辑等价,不用自己写 delegate 声明:
Func<int,int> calc;
Func<int,int>和上面手写的MyCalc是两个完全不同的委托类型,虽然签名一模一样,但是类型不一样,不能直接互相赋值,需要转换。MyCalc d1 = x=>x*2; Func<int,int> d2 = d1; // 编译报错,类型不同!哪怕签名相同,不同 delegate 类型属于不同类型,不能隐式转换。这一点很关键。
理清概念分层
- delegate:关键字,用来声明委托类型。
- Func<> / Action<>:BCL 预先用
delegate关键字写好的一堆泛型委托类型,直接复用。 - Lambda
x=>...:这才是语法糖,编译器把 lambda 生成匿名方法,可以赋值给匹配签名的委托(可以赋值给 Func、Action、也可以赋值给你自己手写 delegate)。
通俗总结
delegate:造委托类型的模具Func<>:微软已经用模具造好一批现成委托类型,直接用,不用你再拿模具造lambda:写委托实例的简写语法糖
Predicate 补充
Predicate<T>也是预定义委托:delegate bool Predicate<T>(T obj),等价于Func<T,bool>,但是独立类型,二者也不能直接互赋值。
小例子区分
//1.自己造委托类型 delegate int MyDel(int a); //2.使用BCL现成委托类型Func Func<int, int> funcDel; //3.lambda语法糖,赋值给任意匹配签名委托 MyDel d1 = x => x + 1; Func<int, int> d2 = x => x + 1; // d1 = d2; //编译错误!类型不一样 // d2 = d1; //编译错误!类型不一样 //只能通过lambda/匿名方法中转 Func<int,int> d3 = d1.Invoke;关键点记忆
Func不是简化 delegate 声明的语法糖,是现成 delegate 类型;lambda 才是语法糖。 签名一样,不同委托类型仍然是不同类型,不能隐式互换。
如果你需要,我可以讲下匿名方法、lambda、Func、自定义 delegate 完整演进对比。
delegate 范围比 func 大?大在哪里
delegate 范围比 Func<> 大在哪里
一句话:delegate是 C# 关键字,用来定义任意委托类型;Func<>只是.NET 预先定义好的一小部分委托实例。delegate能做的,Func 不一定能做;Func 只是 delegate 的子集。
类比:
delegate= class 关键字(用来定义类)Func<>= .NET 已经写好的某个类,你直接 new 使用。
1. Func 有硬性限制
Func 家族全部遵循:
- 返回值不能是 void;void 交给 Action
- 最多16 个泛型参数(
Func<T1…T16,TResult>) - 返回值必须放在最后一个泛型位置
也就是说:Func 只能表达:0‑16 个参数 + 带返回值的方法签名。
delegate 没有这些限制,这就是它更大的地方
① 可以定义 ref /out 参数的委托,Func 做不到
BCL 内置的Func/Action全部不支持 ref、out。 如果你需要委托带 out/ref,只能自己delegate。
// ✅自定义delegate支持 out delegate bool TryParseDel(string s, out int val); // ❌ Func<string, int, bool> 无法表达 out int,没有语法可以标记outFunc 泛型参数只能代表类型,不能修饰参数修饰符
ref/out/in。这是最常用的差距。
② 可以定义返回 void,也可以自定义语义名称
Func 不能 void 返回;void 用 Action。 但自定义 delegate 可以给委托起业务含义名字:
// 语义清晰:代表事件回调 public delegate void DataChangedEventHandler(object sender, DataChangedArgs e);用Action<object, DataChangedArgs>也能装,但名字看不出业务含义。
③ 可以超过 16 个参数
Func 最多 16 个入参。 如果真的需要 17 个参数的委托,只能手写 delegate。(现实极少遇到)
④ 可以定义静态委托、绑定到实例,还能用于事件
C# 的event事件底层依赖 delegate 类型。
你可以把事件声明为自定义 delegate; 虽然现在也常用
EventHandler<T>(也是内置 delegate),但不能直接把 event 声明成 Func。
// 合法 public event DataChangedEventHandler OnDataChange; // ❌编译报错,event不能用Func<> public event Func<int> OnDataChange;事件语法要求委托类型,Func 是委托类型,但C# 语法禁止 event 使用 Func/Action,因为事件设计初衷是多播回调,Func 有返回值会产生歧义(多个返回值取哪个?)。
⑤ 可以定义泛型约束,Func 是固定写死的
自定义 delegate 可以加泛型约束:
delegate T CreateDel<T>() where T : new();内置 Func 是 BCL 已经写死,你不能修改它的泛型约束。
范围关系图
delegate关键字(可以定义任意委托类型) ├─ 自己手写 delegate MyDel(...) 【完全自由:ref/out、命名、约束、事件】 └─ .NET内置预定义委托 ├─ Func<T…> :0‑16参数,有返回值,不能ref/out ├─ Action<T…> :0‑16参数,void返回,不能ref/out └─ Predicate<T>、EventHandler<T> 等关键误区纠正
“Func 是 delegate” —— 这句话对:Func 本质就是用 delegate 关键字声明出来的类型。 “delegate 就是 Func” —— 错:delegate 是定义工具,Func 是工具产出的其中一个成品。
什么时候用哪个?
- 普通回调、LINQ、lambda:优先
Func/Action,省事。 - 需要
ref/out参数:必须手写 delegate。 - 写事件:优先
EventHandler<T>,不要 Func/Action。 - 需要业务语义名称、泛型约束:自定义 delegate。
补充小坑
即便签名完全一致:
delegate int MyDel(int x); Func<int, int> f;MyDel和Func<int,int>是两个完全不同的委托类型,不能隐式转换,只能通过 lambda 或者.Invoke中转。
如果你需要,我可以举个 ref 参数委托完整可运行示例。
func 是委托的语法糖?
结论先说
Func<> 不是委托的语法糖。
delegate是关键字,用来定义委托类型。Func<T1,TResult>是.NET BCL 已经用 delegate 定义好的委托类型,是一个实实在在的类型,不是编译器帮你偷偷生成的简写。- Lambda
x=>x+1才是语法糖。
拆开看
这个是源码级别的定义,.NET 内部就是这么写的:
public delegate TResult Func<TResult>(); public delegate TResult Func<T1, TResult>(T1 arg1);这一行是类库代码,不是编译器看到你写
Func<>之后临时生成出来的。
什么叫语法糖?
语法糖:你写简短写法,C# 编译器在编译阶段自动帮你展开成等价代码,源码里不存在那个简写对应的类型。
举例:lambda 就是语法糖:
Func<int,int> f = x => x * 2;编译器会把这个 lambda 翻译成匿名方法,再赋值给委托对象。这是糖。
但Func<int,int>本身不是糖,它就是一个已经存在的委托类型,就跟你自己写:
public delegate int MyDelegate(int x);产出的MyDelegate地位完全一样:都是委托类型,只是一个是微软写好放库里面,一个是你自己写。
很多人会混淆的点
“用 Func 就不用手写 delegate 了,这不就是语法糖吗?”
不是语法糖,这只是复用现成类型,省去你手动声明委托类型的工作量。
关键证据:
- 相同签名,不同委托类型不能隐式转换
delegate int MyDel(int x); MyDel d1 = x=>1; Func<int,int> d2 = d1; // ❌编译报错,类型不一样如果 Func 只是语法糖,编译器应该把二者视为同一类型,但实际不是。它们是两个独立的委托类型。
- Func 有硬边界:不支持
ref/out/in参数,最多 16 个参数。 如果它是编译器语法糖,理论上编译器可以帮你生成支持 ref/out 的版本,但它做不到,因为类型是库里面写死的。
层级关系梳理
delegate关键字:制造委托类型的工具- Func/Action:工具已经造好的一批现成委托类型(BCL 内置)
- Lambda
=>:语法糖,编译器生成匿名方法,可赋值给任意匹配签名的委托类型(自定义 delegate、Func、Action 都可以接)
类比:
delegate好比struct关键字DateTime是 .NET 内置的 struct,你直接用- 你不能说 DateTime 是 struct 的语法糖,它只是一个现成结构体。同理 Func 不是 delegate 的语法糖。
一句话区分记忆
- ✅ Lambda
=>:语法糖 - ✅ Func<>:预定义的委托类型,不是语法糖
- ✅ delegate:定义委托类型的关键字
如果你愿意,我可以再对比匿名方法、lambda、Func、自定义 delegate,看一段代码编译后 IL,直观看出差异。
就是 func 可以系统自己识别你的委托类型,而 delegate 要自己设置
你的这个感受非常贴近日常写代码的直觉,但表述要修正一下:
❌不是系统 “识别你的委托类型” ✅是Func<> 是已经存在的现成委托类型,你直接填泛型参数就能用;delegate 关键字是让你全新定义一个委托类型
对比举例
用 delegate:你要先造类型,再用
// 第一步:手动定义一个全新的委托类型 MyCalc delegate int MyCalc(int x); // 第二步:再去声明变量 MyCalc fn = x => x * 2;MyCalc是你创造出来的新类型,程序集里会多出这个类型。
用 Func<>:类型早就有了,你只需要填类型参数
// 没有定义新类型!Func<int,int>这个类型本来就在BCL里 Func<int, int> fn = x => x * 2;你并没有创造新委托类型,只是实例化那个已经存在的泛型委托。
关键误区:它不会自动识别你的方法然后生成委托
很多人以为:我写个 lambda,编译器看我入参返回,自动生成 Func。 不是。是你显式写了Func<int,int>,编译器才知道要把 lambda 赋值给这个已有的委托类型。
// 这行单独写,编译报错!编译器猜不出来你要什么委托类型 var fn = x => x * 2;var fn = x=>x*2;直接报错,lambda 本身没有类型,必须赋值给一个明确的委托类型,可以是:
- 现成的
Func<int,int> - 或者你自己
delegate定义的MyCalc
lambda 本身是 “逻辑片段”,没有自身类型,必须挂靠到某一个委托类型上。
通俗大白话总结你的理解
你的原话:func 可以系统自己识别你的委托类型,而 delegate 要自己设置
修正版:
Func 是系统预先写好一批委托类型,你只要填泛型参数就拿来用,不用自己新建委托类型; delegate 关键字是用来从零新建一个属于你自己的委托类型。 两者都需要显式指定类型,Func 不会自动推断生成委托。
核心差异点
delegate A(...):定义新委托类型,输出一个新类型到程序集Func<TIn,TOut>:使用已经存在的委托类型,不产生新类型- Lambda:无类型,只能赋值给上面两种委托类型之一
再强调那个坑
哪怕签名一模一样:
delegate int MyCalc(int x); MyCalc a = x=>x+1; Func<int,int> b = a; // 编译报错!类型不一样,不能直接赋值虽然方法签名完全匹配,但MyCalc和Func<int,int>是两个完全不同的类型,不能隐式转换。 如果 Func 是 “自动识别生成”,那这行就应该能通过,但现实不行。
什么时候选哪个
- 普通回调、LINQ:直接 Func / Action,不用自己定义委托类型,省事。
- 需要 out/ref 参数、写事件、想要业务类型名字:必须自己
delegate定义新委托类型。
如果你想看,我可以简单讲下 IL 层面,看自定义 delegate 会多出一个类型,而 Func 不会。
这不是一样吗你对比举例的两个
表面写出来的代码看着效果行为一样:都能存一个int→int的方法,调用起来结果一样。 但是类型层面完全不一样,这就是很多人迷惑的根源。
看两段代码:
//版本A:自己delegate delegate int MyCalc(int x); MyCalc fnA = x => x * 2; int resA = fnA(5); //版本B:用Func Func<int, int> fnB = x => x * 2; int resB = fnB(5);运行结果:resA=10,resB=10,执行效果一模一样。
所以你感觉:这不是一样吗? ✔运行行为一样❌但是类型、编译产物、能否互相赋值完全不一样
核心差别:编译之后程序集里面发生了什么
A 方案(自定义 delegate)
delegate int MyCalc(int x);编译器会在你的 dll/exe 里生成一个全新的类型:MyCalc,继承自MulticastDelegate。 这是属于你项目的新类型。
B 方案(Func<int,int>)
Func<int,int>不是你项目生成的,它来自系统程序集System.Private.CoreLib.dll,早就写死在框架里。 你只是引用这个现成类型,你的程序集不会多出任何新委托类型。
👉 一个是 “你造一个新盒子”;一个是 “拿系统现成的盒子”。两个盒子外形尺寸一模一样(装的方法签名一样),但盒子本身不是同一个东西。
证据:不能互相赋值
MyCalc fnA = x => x * 2; Func<int, int> fnB = fnA; // ❌编译报错!盒子长得一样,但是商标不一样,不能直接把 A 盒子的东西交给 B 盒子变量。
如果它们真的 “完全一样”,上面这行就应该编译通过,实际不行。
那为什么 lambda 又可以同时赋值给两者?
MyCalc fnA = x=>x*2; Func<int,int> fnB = x=>x*2;lambda 本身只是一段逻辑,编译器根据左边变量的委托类型,帮你包装成对应委托对象。
- 左边是
MyCalc→ 包装成MyCalc实例 - 左边是
Func<int,int>→ 包装成Func<int,int>实例
lambda 不是生成 Func,它适配左边是什么委托,就生成什么委托实例。
打个生活化比喻
delegate int MyCalc(int x):你自己动手定做一个盒子,规格:输入 int,输出 int,这个盒子归你的项目。Func<int,int>:商店(.NET 框架)已经量产好一模一样规格的盒子,你直接买回来用。
两个盒子装东西的规则完全相同,放进去 5,出来 10。 但是:
- 来源不一样:一个你造,一个系统造
- 类型身份不一样:不能把定做盒子直接交给商店盒子的变量接收
- 定做盒子你可以改规格:比如支持
out/ref;商店量产的 Func 盒子规格固定死改不了。
总结你的困惑
“代码写出来效果一样,那到底差别在哪?”
- 运行调用逻辑:相同
- CLR 类型身份:不同
- 编译输出:自定义 delegate 会新增类型;Func 复用系统已有类型
- 能力边界:自定义 delegate 可以 ref/out;Func 做不到
日常业务写回调,绝大多数场景你感知不到差异,所以很多人误以为 Func 就是 delegate 的简写语法糖。但底层类型模型上二者不能等同。
如果你想看,我可以用伪 IL 展示两者编译后的差别。
func 只能定义 int 类型盒子?
不是,Func<>盒子不是只能 int,泛型可以填任意类型。
//输入string,返回bool Func<string, bool> f1 = s => s.Length > 0; //无参数,返回List<int> Func<List<int>> f2 = () => new List<int>(); //两个输入:string,int,返回DateTime Func<string, int, DateTime> f3 = (str, num) => DateTime.Now;盒子的入参、返回值全部由你填的泛型参数决定,string、class、自定义类、结构体都可以。
但是!盒子的结构是固定死的(重点)
Func 这个 “盒子模具” 是微软写死的:
Func[T1,T2,...,TResult]
- 前面全部是输入参数的类型
- 最后一个永远是返回值类型
- 只能写类型,不能加修饰符:ref /out/in
✅可以随便换什么类型 ❌不能修改参数的修饰符,这是模具本身的限制。
举例子,Func 做不到的
我想要一个委托:接收out int参数
// 设想,但是 C# 不支持!泛型占位符不能标记 out Func<string, out int, bool> // ❌语法错误泛型参数只代表 “是什么类型”,不能写out/ref。这种场景,就必须自己用 delegate 造新盒子:
delegate bool MyTryParse(string s, out int value);这个自己造的盒子,我可以自由加 out/ref/in,这是 Func 量产盒子做不到的。
再回顾比喻
Func<>:商店量产的一批盒子。- 盒子的内部卡槽结构固定:若干输入槽,一个输出槽;卡槽只能放类型,不支持 ref/out 卡扣。
- 但是卡槽里放什么类型(int、string、自定义类)由你自己选。
- 手写
delegate:你自己开模做盒子。- 卡槽数量你定,还可以额外加上 ref/out 卡扣。卡槽放什么类型也你定。
Func 限制的不是里面放什么数据类型,而是盒子本身的参数修饰语法结构。
再看一个自定义类示例,Func 完全能用
public class User { public string Name; } //输入User对象,返回string Func<User, string> getName = u => u.Name;没问题,随便用自定义 class。
Func 还有一个硬限制
最多16 个输入参数,Func<T1,T2…T16,TResult>,超过 16 个入参,Func 这套模具就没有了,只能手写 delegate。
极简小结
- Func 泛型可以填任意数据类型,不限于 int。
- 限制不在类型,在于参数修饰符 ref/out/in 不支持、最多 16 入参。
- 一旦需要 ref/out,只能 delegate 自己造盒子。
delegate 和 func 分别支持的数据类型帮我归纳下
delegate vs Func<> 支持的数据 / 参数能力归纳
注意:这里说的不是能传什么数据类型(int/string/class 都支持),而是参数修饰符、参数数量、返回值形式,普通数据类型两者全都支持。
1️⃣ Func<>(系统预定义委托)
底层:public delegate TResult Func<T1,T2,...,TResult>(T1 arg1, T2 arg2…)
表格
| 项目 | Func<> |
|---|---|
| 普通值类型 | ✅ int、bool、DateTime、struct 全部支持 |
| 引用类型 | ✅ string、自定义 class、List<T> 全部支持 |
| 返回值 | ✅必须有返回值,不能 void;void 请用 Action |
| 参数数量 | ✅ 0~16 个输入参数;超过 16 个❌不支持 |
ref参数 | ❌完全不支持 |
out参数 | ❌完全不支持 |
in参数 | ❌完全不支持 |
| 泛型约束 | ❌不能自定义约束,框架已经写死 |
| 用于 event 事件 | ❌语法禁止使用 Func 做事件类型 |
| 自定义委托名称 | ❌不能起业务语义名字,固定叫 Func |
关键点:Func 的泛型占位符只能描述是什么类型,无法携带 ref/out/in 修饰符。
Func<string, int, bool> ok; //正常 Func<string, out int, bool> error; //语法报错,不允许写out2️⃣ delegate 关键字(自定义委托)
delegate 是用来定义新委托类型的关键字,你自己写签名。
表格
| 项目 | 自定义 delegate |
|---|---|
| 普通值类型 | ✅ int、bool、struct 全部支持 |
| 引用类型 | ✅ string、自定义 class 全部支持 |
| 返回值 | ✅ 可以返回任意类型,也可以返回 void |
| 参数数量 | ✅ 理论无上限,写多少个参数都行 |
ref参数 | ✅ 原生支持 |
out参数 | ✅ 原生支持 |
in参数 | ✅ 原生支持 |
| 泛型约束 | ✅ 可以写where T:xxx泛型约束 |
| 用于 event 事件 | ✅ 完全支持,事件标准用法 |
| 自定义委托名称 | ✅ 可以给委托起业务名字,例如TryParseHandler |
示例:
// out、ref、void、泛型约束全都可以 public delegate bool TryHandler<T>(string input, out T result) where T : struct;3️⃣ 一张表看清哪些场景该选谁
表格
| 场景 | 选 Func/Action | 必须手写 delegate |
|---|---|---|
| 普通回调、LINQ,无 ref/out | ✅ | ❌ |
| 回调需要 out /ref/in 参数 | ❌ | ✅ |
| 写事件 (event) | ❌ | ✅(优先 EventHandler<T>) |
| 入参超过 16 个 | ❌ | ✅ |
| 想要委托带业务语义名称 | ❌ | ✅ |
| 需要自定义泛型约束 | ❌ | ✅ |
4️⃣容易混淆的点澄清
- int、string、自定义类,两种都可以用,没有区别限制不在于你传什么数据,而在于参数的修饰符。
- Func 本质上也是 delegate,是微软用 delegate 关键字定义好的一批委托;只是它的签名模板固定死了,不支持 ref/out。
- Action:Func 的兄弟,无返回值,同样不支持 ref/out/in。
极简记忆口诀
Func/Action:类型随便填,ref/out 不能玩;最多 16 参数,事件不能干。 自定义 delegate:签名我做主,ref/out 随便撸;事件约束泛型,全部都能铺。