C# 委托、Action、Func、Event、Lambda 表达式详解(豆包总结)
在 C# 中,委托、Action、Func、Event、Lambda 表达式是 面向对象+函数式编程 的核心特性,它们共同解决了「方法作为参数传递」「解耦组件通信」等关键问题。本文将从「概念本质→核心用途→经典示例→关联区别→补充特性」逐步拆解,确保通俗易懂。
一、委托(Delegate):方法的"类型安全指针"
1. 核心概念
委托是一种 引用类型,它专门用来「封装具有特定签名(参数列表+返回值)的方法」—— 可以把委托理解为「方法的契约」:只有符合契约的方法(参数类型/数量、返回值一致)才能被委托引用。
委托的核心价值:将方法作为参数传递,实现代码的灵活性和解耦(比如回调函数场景)。
2. 定义与使用步骤
步骤1:定义委托(声明"方法契约")
语法:delegate 返回值类型 委托名(参数列表);
步骤2:实现符合契约的方法
步骤3:创建委托实例并绑定方法
步骤4:调用委托(间接调用绑定的方法)
3. 经典示例:委托实现"计算回调"
需求:实现一个通用计算方法,支持动态传入「加法/减法/乘法」逻辑。
using System;
// 1. 定义委托(契约:接收两个int,返回int)
public delegate int CalculateDelegate(int a, int b);
class Program
{
// 2. 实现符合契约的具体方法
static int Add(int x, int y) => x + y;
static int Subtract(int x, int y) => x - y;
static int Multiply(int x, int y) => x * y;
// 3. 通用计算方法(接收委托作为参数,实现逻辑解耦)
static int Calculate(int a, int b, CalculateDelegate calculateFunc)
{
// 委托为null时调用会报错,需判空(C# 6+ 可简化为 calculateFunc?.Invoke(a, b))
if (calculateFunc == null)
throw new ArgumentNullException(nameof(calculateFunc));
return calculateFunc.Invoke(a, b); // 调用委托绑定的方法
}
static void Main()
{
// 4. 创建委托实例并绑定方法(3种写法,效果一致)
CalculateDelegate addDelegate = Add; // 直接绑定静态方法
CalculateDelegate subtractDelegate = new CalculateDelegate(Subtract); // 显式实例化
CalculateDelegate multiplyDelegate = (x, y) => x * y; // 用Lambda表达式绑定(简化)
// 5. 调用通用计算方法,动态传入不同逻辑
Console.WriteLine("3+5=" + Calculate(3, 5, addDelegate)); // 输出 8
Console.WriteLine("10-4=" + Calculate(10, 4, subtractDelegate)); // 输出 6
Console.WriteLine("6*7=" + Calculate(6, 7, multiplyDelegate)); // 输出 42
}
}
4. 委托的关键特性
- 类型安全:只能绑定与委托签名一致的方法(参数、返回值必须匹配),编译时会校验。
- 多播委托:一个委托可以绑定多个方法(通过
+=添加,-=移除),调用时按绑定顺序依次执行。// 多播委托示例 CalculateDelegate multiDelegate = Add; multiDelegate += Multiply; // 绑定第二个方法 multiDelegate(2, 3); // 先执行 Add(2,3)=5,再执行 Multiply(2,3)=6(返回值取最后一个方法的结果) multiDelegate -= Add; // 移除Add方法 - 支持静态方法和实例方法:委托可绑定静态方法(如上述示例)或实例方法。
class Calculator { public int Divide(int x, int y) => x / y; // 实例方法 } // 绑定实例方法 Calculator calc = new Calculator(); CalculateDelegate divideDelegate = calc.Divide; Console.WriteLine("8/2=" + divideDelegate(8, 2)); // 输出 4
二、Action:无返回值的"简化委托"
1. 核心概念
Action 是 .NET 框架 预定义的委托,专门用于封装「无返回值(void)」的方法。它的本质是「系统帮你写好了的委托」,避免重复定义无返回值委托的冗余代码。
2. 重载形式(支持0~16个参数)
Action:无参数、无返回值Action<T>:1个参数(类型T)、无返回值Action<T1, T2>:2个参数(类型T1、T2)、无返回值- ... 最多支持
Action<T1, T2, ..., T16>(16个参数)
3. 经典示例:Action实现"任务执行器"
需求:实现一个通用方法,执行不同的"无返回值任务"(如打印日志、发送通知)。
using System;
class Program
{
// 通用任务执行器(接收Action委托,无返回值)
static void ExecuteTask(Action task)
{
Console.WriteLine("任务开始...");
task?.Invoke(); // 执行委托绑定的任务
Console.WriteLine("任务结束!\n");
}
// 具体任务方法(无返回值,符合Action契约)
static void PrintLog() => Console.WriteLine("执行日志打印:操作成功");
static void SendNotification(string msg) => Console.WriteLine($"发送通知:{msg}");
static void Main()
{
// 1. 调用无参数Action
ExecuteTask(PrintLog); // 绑定静态方法
// 2. 调用带1个参数的Action<string>
Action<string> notifyAction = SendNotification;
notifyAction("用户登录成功"); // 直接调用:输出「发送通知:用户登录成功」
// 3. 用Lambda表达式简化(无需定义单独方法)
ExecuteTask(() => Console.WriteLine("执行临时任务:清理缓存"));
}
}
输出结果:
任务开始...
执行日志打印:操作成功
任务结束!
发送通知:用户登录成功
任务开始...
执行临时任务:清理缓存
任务结束!
三、Func:有返回值的"简化委托"
1. 核心概念
Func 是 .NET 框架 预定义的委托,专门用于封装「有返回值」的方法。与 Action 互补,避免重复定义有返回值委托的冗余代码。
2. 重载形式(支持0~16个参数,最后一个类型是返回值类型)
Func<TResult>:无参数、返回值类型为TResultFunc<T, TResult>:1个参数(类型T)、返回值类型为TResultFunc<T1, T2, TResult>:2个参数(类型T1、T2)、返回值类型为TResult- ... 最多支持
Func<T1, T2, ..., T16, TResult>(16个参数+1个返回值)
3. 经典示例:Func实现"数据转换器"
需求:实现一个通用转换器,将int类型数据转换为不同类型(字符串、double、bool)。
using System;
class Program
{
// 通用转换器(接收Func<int, TResult>,输入int,返回TResult)
static TResult ConvertData<TResult>(int input, Func<int, TResult> converter)
{
Console.WriteLine($"转换输入:{input}");
return converter?.Invoke(input) ?? default(TResult); // 执行转换并返回结果
}
static void Main()
{
// 1. int→string(用Lambda简化,无需定义单独方法)
string strResult = ConvertData(123, num => num.ToString("D5")); // D5:补零到5位
Console.WriteLine($"转换结果(string):{strResult}\n"); // 输出 00123
// 2. int→double(绑定静态方法)
Func<int, double> doubleConverter = NumToDouble;
double doubleResult = ConvertData(45, doubleConverter);
Console.WriteLine($"转换结果(double):{doubleResult}\n"); // 输出 45.5
// 3. int→bool(判断是否为偶数)
bool boolResult = ConvertData(7, num => num % 2 == 0);
Console.WriteLine($"转换结果(bool,是否偶数):{boolResult}"); // 输出 False
}
// 静态转换方法(符合Func<int, double>契约)
static double NumToDouble(int num) => num + 0.5;
}
4. Action vs Func:核心区别
| 特性 | Action | Func |
|---|---|---|
| 返回值 | 无(void) | 有(最后一个泛型是返回值) |
| 适用场景 | 执行"操作"(如打印、通知) | 执行"计算/转换"(如求和、类型转换) |
| 泛型参数 | 仅参数类型(0~16个) | 参数类型+返回值类型(0~16个参数) |
四、Lambda 表达式:匿名方法的"语法糖"
1. 核心概念
Lambda 表达式是 匿名方法的简化语法,本质是「没有名称的方法」,可直接作为委托(Delegate/Action/Func)的参数或返回值。它的核心价值是 简化代码,避免定义冗余的命名方法。
2. 语法格式
(参数列表) => 表达式/代码块
=>:Lambda 运算符,读作"goes to"- 若参数列表只有1个参数,可省略括号(如
num => num * 2) - 若无参数,需写空括号(如
() => Console.WriteLine("Hello")) - 若表达式是多行代码,需用大括号
{}包裹,且需显式写return(单行表达式可省略return)
3. 经典示例:Lambda 简化委托使用
结合前面的委托/Action/Func,看 Lambda 如何简化代码:
using System;
class Program
{
static void Main()
{
// 1. 简化委托调用(无需定义Add方法)
CalculateDelegate addDelegate = (x, y) => x + y;
Console.WriteLine("3+5=" + addDelegate(3, 5)); // 输出 8
// 2. 简化Action(无参数)
Action printAction = () => Console.WriteLine("Hello Lambda!");
printAction(); // 输出 Hello Lambda!
// 3. 简化Func(带参数+返回值)
Func<int, int, int> multiplyFunc = (a, b) => a * b;
Console.WriteLine("4*6=" + multiplyFunc(4, 6)); // 输出 24
// 4. 多行Lambda(需{}和return)
Func<int, string> describeFunc = num =>
{
if (num > 0) return $"正数:{num}";
else if (num < 0) return $"负数:{num}";
else return "零";
};
Console.WriteLine(describeFunc(-3)); // 输出 负数:-3
}
}
// 复用前面定义的委托
public delegate int CalculateDelegate(int a, int b);
4. Lambda 的关键特性
- 类型推断:编译器会根据上下文(如委托类型)自动推断参数类型和返回值类型,无需显式声明(如
(x, y) => x + y中,x和y的类型由委托决定)。 - 捕获变量:Lambda 可以访问外层作用域的变量(值类型捕获副本,引用类型捕获引用)。
int factor = 2; Func<int, int> multiplyByFactor = num => num * factor; Console.WriteLine(multiplyByFactor(5)); // 输出 10(捕获了factor变量) factor = 3; Console.WriteLine(multiplyByFactor(5)); // 输出 15(引用类型捕获,值会变化) - 仅能赋值给委托:Lambda 本身没有类型,必须赋值给具体的委托类型(Delegate/Action/Func)才能使用。
五、事件(Event):基于委托的"发布-订阅模式"
1. 核心概念
事件(Event)是 委托的"安全包装器",基于「发布-订阅(Publish-Subscribe)」模式,用于组件间的解耦通信(如按钮点击、消息通知)。
事件的核心价值:限制委托的访问权限—— 外部只能通过 += 订阅、-= 取消订阅,不能直接调用委托或覆盖绑定(避免恶意修改)。
2. 本质理解
事件 = 委托字段 + 访问控制(仅暴露订阅/取消订阅接口)。如果直接用委托字段,外部可以:
- 直接调用委托(
delegateInstance()),破坏封装; - 用
=覆盖已绑定的方法(而非+=添加),导致之前的订阅失效。
事件解决了这些问题,是委托的「安全使用场景」。
3. 定义与使用步骤
步骤1:定义委托(事件的"通信契约")
步骤2:用 event 关键字声明事件(基于委托)
步骤3:发布者(Publisher):定义触发事件的方法(通常是 OnXXX 命名)
步骤4:订阅者(Subscriber):通过 += 订阅事件,-= 取消订阅
步骤5:发布者触发事件,所有订阅者的方法会被自动调用
4. 经典示例:按钮点击事件(WinForm核心场景)
模拟 WinForm 按钮点击的事件机制,理解发布-订阅模式:
using System;
// 1. 定义委托(事件契约:无参数、无返回值)
public delegate void ButtonClickHandler();
// 发布者(Publisher):按钮组件
public class Button
{
// 2. 声明事件(基于委托,外部仅能订阅/取消订阅)
public event ButtonClickHandler OnClick;
// 3. 触发事件的方法(通常是protected virtual,允许子类重写)
protected virtual void RaiseClick()
{
// 事件触发前需判空(无订阅者时为null)
OnClick?.Invoke(); // 触发事件,所有订阅者的方法会执行
}
// 模拟用户点击按钮(外部调用此方法触发事件)
public void Click()
{
Console.WriteLine("用户点击了按钮...");
RaiseClick(); // 内部调用触发方法
}
}
// 订阅者1(Subscriber):日志组件
public class LogManager
{
// 订阅事件的方法(符合ButtonClickHandler契约)
public void OnButtonClicked()
{
Console.WriteLine("日志组件:记录按钮点击事件\n");
}
}
// 订阅者2(Subscriber):通知组件
public class NotificationManager
{
public void OnButtonClicked()
{
Console.WriteLine("通知组件:弹出点击成功提示\n");
}
}
class Program
{
static void Main()
{
// 实例化发布者和订阅者
Button btn = new Button();
LogManager log = new LogManager();
NotificationManager notify = new NotificationManager();
// 4. 订阅事件(+= 绑定方法)
btn.OnClick += log.OnButtonClicked;
btn.OnClick += notify.OnButtonClicked;
// 5. 模拟用户点击按钮(触发事件)
btn.Click();
// 6. 取消订阅(-= 移除方法)
btn.OnClick -= notify.OnButtonClicked;
Console.WriteLine("通知组件已取消订阅,再次点击按钮:");
btn.Click();
}
}
输出结果:
用户点击了按钮...
日志组件:记录按钮点击事件
通知组件:弹出点击成功提示
通知组件已取消订阅,再次点击按钮:
用户点击了按钮...
日志组件:记录按钮点击事件
5. 事件的关键特性
- 访问权限控制:外部只能
+=/-=,不能直接调用OnClick()或用=覆盖。 - 多订阅者支持:一个事件可以有多个订阅者,触发时按订阅顺序依次执行。
- 标准事件模式:.NET 推荐使用
EventHandler委托(自带 sender 和 EventArgs 参数),而非自定义委托:
优点:统一事件格式,支持传递事件上下文(如点击位置、消息内容)。// 标准事件委托(sender:事件源,e:事件参数) public delegate void EventHandler(object sender, EventArgs e); // 声明标准事件 public event EventHandler OnClick; // 触发时传递事件源和参数 protected virtual void RaiseClick() { OnClick?.Invoke(this, EventArgs.Empty); // this:事件源(按钮本身) }
六、关联特性补充
1. 匿名方法(Anonymous Method)
Lambda 表达式的「前身」,语法更繁琐,现在已基本被 Lambda 替代。语法:delegate(参数列表) { 代码块; }
// 匿名方法示例(绑定到Action)
Action printAction = delegate() { Console.WriteLine("匿名方法"); };
printAction();
// 等价于 Lambda 表达式
Action lambdaAction = () => Console.WriteLine("Lambda 表达式");
适用场景:仅当需要显式忽略参数(如 delegate { ... } 无参数列表)时,匿名方法更简洁。
2. 泛型委托的协变与逆变
协变(out):允许返回值类型「从子类向父类转换」;
逆变(in):允许参数类型「从父类向子类转换」。
Action 和 Func 已内置协变/逆变支持:
Func<out TResult>:TResult 支持协变;Action<in T>:T 支持逆变。
示例:
// 协变示例(返回值:Dog→Animal)
Func<Dog> getDog = () => new Dog();
Func<Animal> getAnimal = getDog; // 允许,因为 Dog 是 Animal 的子类
// 逆变示例(参数:Animal→Dog)
Action<Dog> feedDog = dog => Console.WriteLine("喂狗");
Action<Animal> feedAnimal = feedDog; // 允许,因为 Dog 是 Animal 的子类
public class Animal { }
public class Dog : Animal { }
3. 委托的 Combine 与 Remove 方法
手动操作多播委托的绑定/解绑(等价于 +=/-=):
CalculateDelegate add = Add;
CalculateDelegate multiply = Multiply;
// 组合委托(等价于 add += multiply)
CalculateDelegate combined = (CalculateDelegate)Delegate.Combine(add, multiply);
combined(2, 3); // 执行 Add 和 Multiply
// 移除委托(等价于 combined -= add)
CalculateDelegate removed = (CalculateDelegate)Delegate.Remove(combined, add);
removed(2, 3); // 仅执行 Multiply
4. 异步委托(BeginInvoke/EndInvoke)
早期用于异步执行方法的委托特性(现在推荐用 async/await),通过 IAsyncResult 获取异步执行结果:
// 异步执行委托
CalculateDelegate add = Add;
IAsyncResult result = add.BeginInvoke(3, 5, null, null); // 异步执行 Add(3,5)
// 做其他事情...
int sum = add.EndInvoke(result); // 等待异步完成并获取结果
Console.WriteLine(sum); // 输出 8
七、核心特性总结表
| 特性 | 本质 | 核心用途 | 关键语法/关键字 |
|---|---|---|---|
| 委托(Delegate) | 方法的类型安全指针 | 封装方法,作为参数传递 | delegate 关键字 |
| Action | 预定义无返回值委托 | 执行无返回值操作 | Action<T1,...T16> |
| Func | 预定义有返回值委托 | 执行计算/转换,返回结果 | Func<T1,...T16,TResult> |
| Lambda 表达式 | 匿名方法的语法糖 | 简化委托绑定,避免冗余方法 | (参数) => 表达式 |
| 事件(Event) | 委托的安全包装器 | 组件间解耦通信(发布-订阅) | event 关键字 |
八、最佳实践
- 优先使用
Action/Func而非自定义委托(减少冗余代码); - 事件优先使用标准
EventHandler委托(统一格式,支持参数传递); - Lambda 表达式仅用于简单逻辑(复杂逻辑建议用命名方法,提高可读性);
- 委托/事件调用前必须判空(
?.Invoke()),避免空引用异常; - 多播委托注意执行顺序(按绑定顺序执行),且返回值仅取最后一个方法的结果。
通过以上特性,C# 实现了「方法作为一等公民」的编程体验,既保留了面向对象的封装性,又融入了函数式编程的灵活性,是构建复杂应用(如 UI 交互、异步通信)的基础。
浙公网安备 33010602011771号