C# 中的委托(Delegate)是一种特殊的引用类型,它安全地封装了方法的签名和引用。委托本质上是类型安全的函数指针,特别用于实现事件和回调方法。使用委托,可以将方法作为参数传递或赋值给变量,并可以调用委托所引用的方法。在 .NET Framework 中,委托是实现观察者模式和异步编程的基础。
委托的定义
委托的定义类似于方法的定义,但不带方法体。它定义了可以引用的方法的签名。从底层实现来看,编译器会为每个委托声明生成一个继承自 System.MulticastDelegate 的类。
1 | public delegate int MyDelegate(int x, int y); |
上面的代码定义了一个名为 MyDelegate 的委托,它接受两个 int 参数并返回一个 int。编译后,这个委托会被转换为一个完整的类,包含 Invoke、BeginInvoke 和 EndInvoke 方法。
委托的使用
委托的使用通常涉及三个步骤:
- 定义委托:如上所述。
- 创建委托实例:使用与委托签名匹配的方法创建委托实例。
- 调用委托:通过委托实例调用方法。
1 | public class HelloWorld |
值得注意的是,委托实例内部维护了目标对象(Target)和方法指针(Method)。对于静态方法,Target 为 null;对于实例方法,Target 指向具体的对象实例。
委托 vs 事件
在 C# 中,事件(Event)是基于委托的一种封装机制。事件提供了一种发布/订阅模型,允许类或者对象通知其他类或者对象当某些特殊事情发生时(例如,用户点击按钮)。事件的实现通常包括一个私有的委托字段和一个公共的事件成员。事件相对于委托的优势在于:它只允许外部代码订阅和取消订阅,而不能直接调用或赋值,这提供了更好的封装性。
1 | public class MyClass |
委托的多播与链式调用
委托可以使用 += 运算符进行链式调用(多播委托),这意味着可以将多个方法关联到同一个委托实例,当调用委托时,所有关联的方法都会按照它们被添加的顺序依次执行。同样地,可以使用 -= 运算符来移除之前添加的方法。
需要注意的是,对于有返回值的多播委托,只有最后一个被调用方法的返回值会被返回,之前的返回值会被丢弃。如果需要获取所有方法的返回值,可以通过 GetInvocationList() 方法获取委托链中的每个委托并逐一调用。
1 | public delegate int Calculator(int x, int y); |
泛型委托与Lambda演进
C# 还支持泛型委托,如 Func<TResult> 和 Action<T> 系列,它们分别用于表示有返回值和无返回值的方法。这些预定义的泛型委托大大简化了代码,避免了为每种方法签名都定义新的委托类型。
从 C# 2.0 的匿名方法到 C# 3.0 的 Lambda 表达式,委托的使用变得越来越简洁:
1 | // 泛型委托:Func<T1, T2, TResult> 表示接受两个参数并返回一个值 |
Func 和 Action 最多支持 16 个参数,基本能覆盖所有实际场景。如果参数更多,建议重构代码或使用自定义类型封装参数。
委托 vs 接口
虽然委托和接口都能实现回调机制,但它们适用于不同场景:
- 委托适合单一方法的回调、事件处理、简单的策略模式
- 接口适合定义一组相关方法的契约、需要多态性的场景
选择原则:如果只需要调用单个方法,委托更轻量;如果需要定义一组行为契约,接口更合适。
性能考虑
委托的性能通常很好,但有几点值得注意:
- 装箱开销:值类型的实例方法创建委托时会产生装箱
- 多播委托:调用链越长,性能开销越大
- 闭包捕获:Lambda 表达式捕获外部变量会产生额外的内存分配
对于性能敏感的场景,可以考虑对象池、避免不必要的委托创建、或使用函数指针(C# 9.0+)。
C#委托真的非常强大!!!它不仅是事件系统的基石,更是函数式编程、异步编程和 LINQ 的核心机制。
相关链接:
[1] C#中的委托和事件