【C# 进阶】委托、事件与 MVC 过滤器实战解析(含企业级场景代码)
很多学 MVC 的新手会觉得 “过滤器”“事件驱动” 这些词很绕,但其实它们的底层就是委托与事件,搞懂这俩,MVC 里的拦截器、生命周期钩子都能一通百通。,“物业” 是事件发布者,“业主” 是事件订阅者,“囤水 / 洗澡” 是事件处理方法。关键区别:委托是 “主动找方法”,事件是 “被动等通知”—— 事件本质是 “受限制的委托”,防止外部乱调用。,“工人” 是具体的方法,“你告诉中介需求” 就是委
目录
在 C# 开发中,委托、事件是解耦代码的核心机制,而 MVC 过滤器则是实现日志记录、权限校验的常用工具。本文从基础概念到实战场景,带你彻底搞懂这三个知识点的关联与用法,文中代码可直接复制到项目中测试,新手也能快速上手。
一、委托:方法的 “引用类型”
委托本质是一种 “能存储方法引用的类型”,相当于给方法起了一个 “统一的接口”,方便统一调用不同但签名相同的方法。
1.1 基础委托:声明、绑定与调用
先定义一个计算委托(接收两个 int 参数,返回 int),再绑定不同方法实现加减逻辑。
using System;
namespace DelegateEventDemo
{
// 1. 声明委托:确定方法的“签名”(参数类型、返回值类型)
public delegate int CalculateDelegate(int a, int b);
class Program
{
static void Main(string[] args)
{
// 2. 绑定方法到委托(直接绑定静态方法)
CalculateDelegate addCalc = Add;
CalculateDelegate subCalc = Subtract;
// 3. 调用委托(和调用方法一样)
Console.WriteLine($"3+2={addCalc(3, 2)}"); // 输出:3+2=5
Console.WriteLine($"5-2={subCalc(5, 2)}"); // 输出:5-2=3
// 补充:用Lambda表达式直接绑定委托(简化代码,无需单独定义方法)
CalculateDelegate mulCalc = (a, b) => a * b; // 乘法逻辑
CalculateDelegate divCalc = (a, b) =>
{
if (b == 0) throw new DivideByZeroException("除数不能为0");
return a / b;
}; // 除法逻辑(含异常处理)
Console.WriteLine($"4*6={mulCalc(4, 6)}"); // 输出:4*6=24
try
{
Console.WriteLine($"8/2={divCalc(8, 2)}"); // 输出:8/2=4
Console.WriteLine($"8/0={divCalc(8, 0)}"); // 触发异常
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"除法异常:{ex.Message}"); // 捕获并处理异常
}
}
// 委托绑定的方法(签名必须和委托一致)
public static int Add(int x, int y) => x + y;
public static int Subtract(int x, int y) => x - y;
}
}
1.2 多播委托:一个委托绑定多个方法
多播委托能存储多个方法引用,调用时会依次执行所有绑定的方法,但需注意 “异常中断” 和 “返回值覆盖” 问题。
// 多播委托示例(绑定多个方法)
CalculateDelegate multiCalc = Add; // 先绑定加法
multiCalc += Subtract; // 追加减法
multiCalc += mulCalc; // 追加乘法(之前定义的Lambda)
// 问题1:多播委托调用时,仅返回最后一个方法的结果
int result = multiCalc(10, 3);
Console.WriteLine($"多播委托返回值(仅最后一个方法):{result}"); // 输出:30(10*3的结果)
// 问题2:单个方法抛出异常,会中断后续方法执行
// 解决:遍历委托的方法列表,逐个调用并捕获异常
foreach (var method in multiCalc.GetInvocationList())
{
try
{
// 用DynamicInvoke调用方法,参数需和委托签名匹配
int methodResult = (int)method.DynamicInvoke(10, 3);
Console.WriteLine($"方法执行结果:{methodResult}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"除法异常:{ex.Message}(已捕获,不影响后续方法)");
}
catch (Exception ex)
{
Console.WriteLine($"其他异常:{ex.Message}");
}
}
【委托小结】
- 委托是 “方法的容器”,核心作用是解耦调用者和被调用者(比如框架调用用户自定义方法)。
- 多播委托需注意两点:返回值仅取最后一个方法;需遍历调用避免异常中断。
- 实际开发中,简单场景优先用 Lambda 绑定委托,减少单独定义方法的代码量。
二、事件:“受限制的委托”
事件是基于委托的 “封装”,它限制了委托的调用权限(只能在类内部触发,外部只能订阅 / 取消订阅),避免外部代码随意修改委托逻辑。
2.1 事件的完整示例:模拟订单支付通知
以 “用户支付订单后,触发短信、邮件通知” 为例,演示事件的声明、订阅与触发。
public class OrderSystem
{
// 1. 声明委托(定义事件触发时的方法签名)
public delegate void OrderPaidHandler(string orderId);
// 2. 声明事件(用event关键字修饰委托,限制外部权限)
public event OrderPaidHandler OnOrderPaid;
// 3. 类内部方法:触发事件(通常用OnXXX命名)
public void PayOrder(string orderId)
{
Console.WriteLine($"订单{orderId}支付成功");
// 触发事件:先判断是否有订阅者,避免空引用
// 多线程场景下需加锁,确保线程安全
lock (this)
{
OnOrderPaid?.Invoke(orderId);
}
}
}
// 4. 外部代码:订阅事件(绑定通知方法)
class NotificationService
{
// 短信通知方法(符合OrderPaidHandler签名)
public static void SendSms(string orderId)
{
Console.WriteLine($"[短信通知] 订单{orderId}已支付,请注意查收");
}
// 邮件通知方法(符合OrderPaidHandler签名)
public static void SendEmail(string orderId)
{
Console.WriteLine($"[邮件通知] 订单{orderId}支付成功,订单详情已发送至邮箱");
}
}
// 测试事件逻辑
OrderSystem _orderSystem = new OrderSystem();
// 订阅事件:绑定两个通知方法
_orderSystem.OnOrderPaid += NotificationService.SendSms;
_orderSystem.OnOrderPaid += NotificationService.SendEmail;
// 支付订单:触发事件(自动执行所有订阅的方法)
_orderSystem.PayOrder("20240520001");
// 输出结果:
// 订单20240520001支付成功
// [短信通知] 订单20240520001已支付,请注意查收
// [邮件通知] 订单20240520001支付成功,订单详情已发送至邮箱
// 取消订阅:移除邮件通知
_orderSystem.OnOrderPaid -= NotificationService.SendEmail;
_orderSystem.PayOrder("20240520002");
// 输出结果:仅触发短信通知
【事件小结】
- 事件的核心是 “封装委托”,外部只能用+=订阅、-=取消订阅,不能直接调用OnOrderPaid()(避免逻辑被篡改)。
- 多线程场景下,触发事件前需加锁(建议用私有静态锁对象,如private static readonly object _lockObj = new object();),防止并发问题。
- 事件是 “观察者模式” 的 C# 实现,常用于框架通知(如按钮点击、订单状态变更)。
三、MVC 过滤器:事件的 “实际应用”
MVC 过滤器本质是 “基于事件的拦截机制”—— 在控制器 Action 执行前 / 后,自动触发预设的逻辑(如日志、权限校验),底层依赖委托和事件实现。
3.1 自定义 MVC 过滤器:模拟日志 + 权限校验
以 “记录请求日志” 和 “校验用户登录” 为例,实现两个核心过滤器,还原 MVC 过滤器的工作原理。
// 1. 定义过滤器委托(确定过滤器方法的签名)
public delegate void ActionExecutingHandler(string controllerName, string actionName);
public delegate void ActionExecutedHandler(string controllerName, string actionName, string result);
// 2. 定义MVC控制器基类(管理过滤器订阅与触发)
public class BaseController
{
// 声明过滤器事件
public event ActionExecutingHandler OnActionExecuting; // Action执行前触发
public event ActionExecutedHandler OnActionExecuted; // Action执行后触发
// 执行Action的核心方法(自动触发过滤器)
public void ExecuteAction(string controllerName, string actionName)
{
// 步骤1:触发“执行前”过滤器(如权限校验、日志记录)
OnActionExecuting?.Invoke(controllerName, actionName);
// 步骤2:执行实际的Action逻辑
string actionResult = $"{controllerName}控制器的{actionName}方法执行完成";
Console.WriteLine(actionResult);
// 步骤3:触发“执行后”过滤器(如结果日志、异常处理)
OnActionExecuted?.Invoke(controllerName, actionName, actionResult);
}
}
// 3. 实现具体过滤器:日志过滤器+权限过滤器
public class LogFilter
{
// Action执行前:记录请求日志
public void LogBeforeAction(string controllerName, string actionName)
{
Console.WriteLine($"[日志-前置] {DateTime.Now:yyyy-MM-dd HH:mm:ss}:请求 {controllerName}/{actionName}");
}
// Action执行后:记录结果日志
public void LogAfterAction(string controllerName, string actionName, string result)
{
Console.WriteLine($"[日志-后置] {DateTime.Now:yyyy-MM-dd HH:mm:ss}:{result},耗时20ms");
}
}
public class AuthFilter
{
// 权限校验:未登录用户拦截跳转
public void CheckUserLogin(string controllerName, string actionName)
{
// 实际项目中对接Session/Token判断登录状态
bool isUserLogin = false; // 模拟“未登录”状态
if (!isUserLogin)
{
Console.WriteLine($"[权限拦截] {controllerName}/{actionName}:用户未登录,自动跳转至登录页(/Login/Index)");
// 真实MVC中通过filterContext.Result = new RedirectResult("/Login/Index")实现跳转
}
else
{
Console.WriteLine($"[权限校验] {controllerName}/{actionName}:用户已登录,允许执行");
}
}
}
// 4. 测试MVC过滤器流程
// 创建控制器和过滤器实例
BaseController userController = new BaseController();
LogFilter logFilter = new LogFilter();
AuthFilter authFilter = new AuthFilter();
// 订阅过滤器事件(绑定逻辑)
userController.OnActionExecuting += logFilter.LogBeforeAction; // 前置:日志
userController.OnActionExecuting += authFilter.CheckUserLogin; // 前置:权限
userController.OnActionExecuted += logFilter.LogAfterAction; // 后置:日志
// 执行用户控制器的“个人中心”Action
userController.ExecuteAction("User", "Profile");
// 输出结果:
// [日志-前置] 2024-05-20 15:30:00:请求 User/Profile
// [权限拦截] User/Profile:用户未登录,自动跳转至登录页(/Login/Index)
// User控制器的Profile方法执行完成
// [日志-后置] 2024-05-20 15:30:00:User控制器的Profile方法执行完成,耗时20ms
【MVC 过滤器小结】
- 过滤器的核心是 “拦截 Action 生命周期”,通过事件订阅实现 “解耦”(控制器无需关注日志 / 权限的具体实现)。
- 真实 MVC 中,过滤器分 4 类:ActionFilter(Action 前后)、AuthorizationFilter(权限)、ResultFilter(结果处理)、ExceptionFilter(异常捕获),用法与本文示例一致。
- 过滤器执行顺序:
同类型过滤器可通过Order属性设置优先级(值越小越先执行)。
四、常见问题 FAQ(新手必看)
| 常见疑问 | 解答与实战建议 |
|---|---|
| 多播委托怎么获取所有方法的返回值? | 不能直接获取,需遍历GetInvocationList()逐个调用,示例: |
| foreach (var method in multiCalc.GetInvocationList()) { var res = (int)method.DynamicInvoke(10,3); } | |
| 事件为什么必须用委托声明? | 事件是 “委托的包装”,委托定义了事件触发时 “方法的签名”,事件则限制了委托的调用权限,二者缺一不可 |
| MVC 过滤器能全局生效吗? | 可以!真实项目中,在Global.asax的Application_Start中注册全局过滤器,如: |
| GlobalFilters.Filters.Add(new LogFilter());,所有控制器都会触发该过滤器 | |
| 委托和接口有什么区别? | 委托适合 “单方法契约”(如回调),接口适合 “多方法契约”(如类的能力定义);委托支持多播,接口不支持 |
五、互动时间
本文从基础到实战,覆盖了委托、事件与 MVC 过滤器的核心用法,代码可直接复制到 VS 中测试(需选择.NET Framework 4.5 + 或.NET Core 3.1 + 框架)。
如果对你有帮助,点赞 + 收藏本文 ,并在评论区留言:“已收藏 + 你想深入学的 C# 知识点”(比如 “多线程下的委托安全”“MVC 异常过滤器实战”),我会在评论区置顶《委托事件调试避坑指南》(含 3 个企业级调试案例),帮你避开新手常踩的坑!
最后,欢迎关注我的专栏,后续会更新更多 C# 进阶实战内容,一起从 “会用” 到 “精通”!
关于“委托与事件”,你目前最卡壳的是?
- 多播委托的异常处理(一个方法报错,后续全中断)
- 事件订阅后的内存泄漏(不清楚何时该取消订阅)
- 委托/事件与MVC过滤器的绑定逻辑(没理清触发顺序)
- 概念区分(分不清委托和事件的使用场景)
- 其他(评论区补充你的具体问题)
更多推荐


所有评论(0)