在这里插入图片描述

在 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(异常捕获),用法与本文示例一致。
  • 过滤器执行顺序:
AuthorizationFilter
ActionFilter前置
Action
ActionFilter后置
ResultFilter

同类型过滤器可通过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过滤器的绑定逻辑(没理清触发顺序)
  • 概念区分(分不清委托和事件的使用场景)
  • 其他(评论区补充你的具体问题)
Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐