04-C#.Net-委托和事件-学习笔记

一、委托(Delegate)基础

1.1 什么是委托?

委托是一种引用类型,本质上是一个,继承自 MulticastDelegate。它可以封装方法的引用,类似于 C/C++ 中的函数指针,但更加类型安全。

// 委托的定义
public delegate void NoReturnNoPara();
public delegate void NoReturnWithPara(int x, int y);
public delegate int WithReturnNoPara();
public delegate int WithReturnWithPara(out int x, ref int y);

关键点:

  • 委托定义时需要指定参数类型和返回值类型
  • 委托本质是类,会被编译器编译成继承自 MulticastDelegate 的类
  • 委托可以定义在类外部或类内部

1.2 委托的实例化

// 方式1:使用 new 关键字
NoReturnNoPara delegate1 = new NoReturnNoPara(NoReturnNoParaMethod);

// 方式2:直接赋值(语法糖)
NoReturnNoPara delegate2 = NoReturnNoParaMethod;

// 方式3:使用 Lambda 表达式
NoReturnWithPara delegate3 = (x, y) => { Console.WriteLine($"x={x}, y={y}"); };

1.3 委托的执行

// 方式1:使用 Invoke 方法
delegate1.Invoke();

// 方式2:直接调用(语法糖)
delegate1();

// 异步调用(已过时,不推荐)
// delegate1.BeginInvoke(null, null);

1.4 委托的作用和意义

问题场景: 学生问候方式因地域不同而不同(武汉、上海、广东等)

传统方案的问题:

  • 方案1(枚举+switch):业务逻辑耦合,方法不稳定
  • 方案2(多个独立方法):大量重复代码

委托解决方案:

public delegate void SayHiDelegate();

public void SayHiPerfect(SayHiDelegate sayHiDelegate)
{
    Console.WriteLine("问候前,招招手...");
    sayHiDelegate.Invoke();  // 执行传入的具体问候逻辑
}

// 使用
student.SayHiPerfect(student.SayHiWuhan);
student.SayHiPerfect(student.SayHiShanghai);

优势:

  1. 代码稳定:核心方法不需要修改
  2. 逻辑解耦:不同业务逻辑分离
  3. 逻辑重用:去除重复代码

二、框架内置委托

2.1 Action 委托

Action 是无返回值的委托,最多支持 16 个参数。

// 无参数
Action action = () => Console.WriteLine("Hello");

// 有参数
Action<int> action1 = (x) => Console.WriteLine(x);
Action<int, string> action2 = (x, y) => Console.WriteLine($"{x}, {y}");

// 最多16个参数
Action<int, int, int, ..., int> action16 = (p1, p2, ..., p16) => { };

2.2 Func 委托

Func 是有返回值的委托,最多支持 16 个输入参数 + 1 个返回值。

// 无参数,有返回值
Func<int> func = () => 100;

// 有参数,有返回值(最后一个泛型参数是返回值类型)
Func<int, int> func1 = (x) => x * 2;
Func<int, string, int> func2 = (x, y) => x + y.Length;

// 最多16个输入参数 + 1个返回值
Func<int, int, ..., int, string> func17 = (p1, p2, ..., p16) => "result";

为什么要有内置委托?

  • 统一委托类型,避免定义大量自定义委托
  • 减少类型数量(委托本质是类)
  • 提高代码可读性和互操作性

三、多播委托(Multicast Delegate)

3.1 概念

所有委托都继承自 MulticastDelegate,可以通过 +=-= 操作符注册和移除多个方法。

Action action = DoNothing;
action += DoNothingStatic;
action += new Student().Study;
action += Student.StudyAdvanced;
action += () => Console.WriteLine("Lambda");

// 执行时按注册顺序依次调用
action.Invoke();

3.2 移除方法

action -= DoNothing;  // 成功移除

// 注意:以下情况无法移除
action -= new Student().Study;  // 失败:不是同一个实例
action -= () => Console.WriteLine("Lambda");  // 失败:Lambda 每次生成不同方法

移除规则:

  • 从后往前逐个匹配
  • 必须是同一个实例的同一个方法
  • 匹配到后移除并停止继续匹配

3.3 异步执行多播委托

// 多播委托不能直接使用 BeginInvoke
// action.BeginInvoke(null, null);  // 错误

// 正确做法:遍历委托列表
foreach (Action item in action.GetInvocationList())
{
    item.BeginInvoke(null, null);  // 每个方法异步执行
}

四、委托的高级应用:嵌套委托(俄罗斯套娃)

4.1 委托嵌套原理

委托可以包装另一个委托,形成多层嵌套,实现 AOP(面向切面编程)。

public delegate void ShowDelegate();

// 核心业务逻辑
ShowDelegate coreMethod = () => Console.WriteLine("核心业务");

// 第一层包装:添加日志
ShowDelegate withLog = () => {
    Console.WriteLine("前置日志");
    coreMethod.Invoke();
    Console.WriteLine("后置日志");
};

// 第二层包装:添加性能监控
ShowDelegate withMonitor = () => {
    Console.WriteLine("开始监控");
    withLog.Invoke();
    Console.WriteLine("结束监控");
};

withMonitor.Invoke();

4.2 结合特性实现自动装配

public abstract class AbstractMethodAttribute : Attribute
{
    public abstract ShowDelegate Do(ShowDelegate action);
}

public class BeforeMethodAttribute : AbstractMethodAttribute
{
    public override ShowDelegate Do(ShowDelegate action)
    {
        return new ShowDelegate(() => {
            Console.WriteLine("前置处理");
            action.Invoke();
            Console.WriteLine("后置处理");
        });
    }
}

// 使用
[BeforeWriteLog]
[BeforeMethod]
public void Method()
{
    Console.WriteLine("核心业务");
}

// 自动装配执行
ShowDelegate showMethod = () => methodInfo.Invoke(instance, null);
foreach (var attribute in methodInfo.GetCustomAttributes().Reverse())
{
    showMethod = attribute.Do(showMethod);
}
showMethod.Invoke();

应用场景:

  • ASP.NET Core 中间件管道
  • 日志记录
  • 性能监控
  • 事务管理
  • 权限验证

五、事件(Event)

5.1 事件的定义

事件是特殊的委托,使用 event 关键字修饰。

public class Cat
{
    // 委托
    public Action MiaoAction = null;
    
    // 事件
    public event Action MiaoEvent = null;
    
    public void Miao()
    {
        Console.WriteLine("喵~");
        MiaoEvent?.Invoke();  // 只能在类内部触发
    }
}

5.2 委托 vs 事件

特性 委托 事件
本质 特殊的委托
外部赋值 可以 = 赋值 只能 +=-=
外部调用 可以直接调用 不能外部调用
安全性 较低 较高
子类访问 可以 不可以
Cat cat = new Cat();

// 委托可以这样操作
cat.MiaoAction = null;  // 清空所有订阅
cat.MiaoAction();       // 外部直接触发

// 事件只能这样操作
cat.MiaoEvent += Handler;  // 只能添加
cat.MiaoEvent -= Handler;  // 只能移除
// cat.MiaoEvent();        // 错误:不能外部触发
// cat.MiaoEvent = null;   // 错误:不能赋值

5.3 观察者模式(发布-订阅模式)

核心要素:

  1. 发布者(Publisher):定义事件并触发
  2. 订阅者(Subscriber):订阅事件并响应
  3. 订阅关系:通过 += 建立

示例:猫叫引发的连锁反应

public class Cat
{
    public event Action MiaoEvent;
    
    public void Miao()
    {
        Console.WriteLine("猫:喵~");
        MiaoEvent?.Invoke();
    }
}

public class Dog
{
    public void Wang() => Console.WriteLine("狗:汪汪!");
}

public class Mouse
{
    public void Run() => Console.WriteLine("老鼠:快跑!");
}

// 使用
Cat cat = new Cat();
cat.MiaoEvent += new Dog().Wang;
cat.MiaoEvent += new Mouse().Run;
cat.Miao();  // 触发所有订阅者的响应

优势:

  • 职责单一:猫只负责叫,不关心后续反应
  • 降低耦合:猫不依赖狗、老鼠等类
  • 易于扩展:添加新的订阅者不需要修改猫的代码

六、标准事件模式

6.1 标准事件定义

.NET 推荐使用 EventHandlerEventHandler<TEventArgs> 定义事件。

// 自定义事件参数
public class PriceChangeEventArgs : EventArgs
{
    public int OldPrice { get; set; }
    public int NewPrice { get; set; }
}

// 发布者
public class Course
{
    private int _price = 5299;
    
    // 标准事件定义
    public event EventHandler<PriceChangeEventArgs> PriceChanged;
    
    public int Price
    {
        get => _price;
        set
        {
            if (value > _price)
            {
                // 触发事件
                PriceChanged?.Invoke(this, new PriceChangeEventArgs
                {
                    OldPrice = _price,
                    NewPrice = value
                });
            }
            _price = value;
        }
    }
}

// 订阅者
public class Student
{
    public void OnPriceChanged(object sender, EventArgs e)
    {
        var course = (Course)sender;
        var args = (PriceChangeEventArgs)e;
        Console.WriteLine($"课程涨价了!从 {args.OldPrice} 涨到 {args.NewPrice}");
        Console.WriteLine("赶紧购买!");
    }
}

// 使用
Course course = new Course();
course.PriceChanged += new Student().OnPriceChanged;
course.Price = 6299;  // 触发事件

6.2 标准事件模式的特点

  1. 事件处理器签名void Handler(object sender, EventArgs e)

    • sender:事件发布者(谁触发的)
    • e:事件参数(携带什么数据)
  2. 事件参数继承:自定义参数类继承 EventArgs

  3. 命名约定

    • 事件名:动词或动词短语(如 PriceChangedClick
    • 事件参数:XxxEventArgs

七、核心概念总结

7.1 委托的使用场景

  1. 方法内部业务逻辑耦合严重 → 使用委托解耦
  2. 多个方法有大量重复代码 → 使用委托实现逻辑重用
  3. 需要回调机制 → 使用委托传递回调方法
  4. 需要实现策略模式 → 使用委托传递不同策略

7.2 事件的使用场景

  1. 需要通知多个对象某个动作发生 → 使用事件
  2. 需要降低类之间的耦合 → 使用事件实现观察者模式
  3. UI 编程(如 WinForms、WPF)→ 使用事件处理用户交互
  4. 需要保护委托不被外部随意调用 → 使用事件增强安全性

7.3 记忆口诀

  • 委托是盒子:可以装方法,可以传递
  • 多播是链条:一个委托串多个方法
  • 事件是保险:只能订阅,不能乱调
  • 观察者模式:发布订阅,解耦神器

posted @ 2026-03-19 14:44  龙猫•ᴥ•  阅读(2)  评论(0)    收藏  举报