C#委托模式

C# 中的委托(Delegate)是一种特殊的引用类型,它安全地封装了方法的签名和引用。委托本质上是类型安全的函数指针,特别用于实现事件和回调方法。使用委托,可以将方法作为参数传递或赋值给变量,并可以调用委托所引用的方法。在 .NET Framework 中,委托是实现观察者模式和异步编程的基础。

委托的定义

委托的定义类似于方法的定义,但不带方法体。它定义了可以引用的方法的签名。从底层实现来看,编译器会为每个委托声明生成一个继承自 System.MulticastDelegate 的类。

1
public delegate int MyDelegate(int x, int y);

上面的代码定义了一个名为 MyDelegate 的委托,它接受两个 int 参数并返回一个 int。编译后,这个委托会被转换为一个完整的类,包含 InvokeBeginInvokeEndInvoke 方法。

委托的使用

委托的使用通常涉及三个步骤:

  1. 定义委托:如上所述。
  2. 创建委托实例:使用与委托签名匹配的方法创建委托实例。
  3. 调用委托:通过委托实例调用方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class HelloWorld
{
// 定义委托
public delegate int MyDelegate(int x, int y);

// 静态方法,与委托签名匹配
public static int Add(int a, int b)
{
return a + b;
}

public static void Main()
{
// 创建委托实例,引用 Add 方法
MyDelegate myDelegate = new MyDelegate(Add);

// 也可以使用简化语法(C# 2.0+)
// MyDelegate myDelegate = Add;

// 调用委托,实际调用的是 Add 方法
int result = myDelegate(5, 3);
// 等价于 int result = myDelegate.Invoke(5, 3);

Console.WriteLine(result); // 输出 8
}
}

值得注意的是,委托实例内部维护了目标对象(Target)和方法指针(Method)。对于静态方法,Target 为 null;对于实例方法,Target 指向具体的对象实例。

委托 vs 事件

在 C# 中,事件(Event)是基于委托的一种封装机制。事件提供了一种发布/订阅模型,允许类或者对象通知其他类或者对象当某些特殊事情发生时(例如,用户点击按钮)。事件的实现通常包括一个私有的委托字段和一个公共的事件成员。事件相对于委托的优势在于:它只允许外部代码订阅和取消订阅,而不能直接调用或赋值,这提供了更好的封装性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class MyClass
{
// 定义委托
public delegate void MyEventHandler(object sender, EventArgs e);

// 声明事件
public event MyEventHandler MyEvent;

// 触发事件的方法
protected virtual void OnMyEvent(EventArgs e)
{
// C# 6.0+ 可以使用 null 条件运算符简化
MyEvent?.Invoke(this, e);

// 传统写法(线程安全)
// MyEventHandler handler = MyEvent;
// if (handler != null)
// {
// handler(this, e);
// }
}

// 其他方法,在适当的时候调用 OnMyEvent
}

// 订阅事件的类
public class MySubscriber
{
public void HandleMyEvent(object sender, EventArgs e)
{
Console.WriteLine("MyEvent was raised!");
}
}

// 使用示例
public class HelloWorld
{
public static void Main()
{
MyClass myObject = new MyClass();
MySubscriber subscriber = new MySubscriber();

// 订阅事件
myObject.MyEvent += subscriber.HandleMyEvent;

// 假设在某个地方触发了事件
myObject.OnMyEvent(EventArgs.Empty); // 输出 "MyEvent was raised!"

// 取消订阅,避免内存泄漏
myObject.MyEvent -= subscriber.HandleMyEvent;
}
}

委托的多播与链式调用

委托可以使用 += 运算符进行链式调用(多播委托),这意味着可以将多个方法关联到同一个委托实例,当调用委托时,所有关联的方法都会按照它们被添加的顺序依次执行。同样地,可以使用 -= 运算符来移除之前添加的方法。

需要注意的是,对于有返回值的多播委托,只有最后一个被调用方法的返回值会被返回,之前的返回值会被丢弃。如果需要获取所有方法的返回值,可以通过 GetInvocationList() 方法获取委托链中的每个委托并逐一调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public delegate int Calculator(int x, int y);

public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;

Calculator calc = Add;
calc += Multiply;

// 只会返回 Multiply 的结果(15),Add 的结果(8)被丢弃
int result = calc(5, 3);

// 获取所有返回值
foreach (Calculator del in calc.GetInvocationList())
{
Console.WriteLine(del(5, 3)); // 先输出 8,再输出 15
}

泛型委托与Lambda演进

C# 还支持泛型委托,如 Func<TResult>Action<T> 系列,它们分别用于表示有返回值和无返回值的方法。这些预定义的泛型委托大大简化了代码,避免了为每种方法签名都定义新的委托类型。

从 C# 2.0 的匿名方法到 C# 3.0 的 Lambda 表达式,委托的使用变得越来越简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 泛型委托:Func<T1, T2, TResult> 表示接受两个参数并返回一个值
Func<int, int, int> add = (a, b) => a + b;
int sum = add(5, 3); // sum 现在是 8

// Action<T> 表示接受参数但无返回值
Action<string> display = (message) => Console.WriteLine(message);
display("Hello, World!"); // 输出 "Hello, World!"

// C# 2.0 匿名方法写法(已过时但仍支持)
Func<int, int, int> multiply = delegate(int a, int b) { return a * b; };

// Lambda 表达式还支持表达式树,这是 LINQ 的基础
Expression<Func<int, int, int>> expression = (a, b) => a + b;

FuncAction 最多支持 16 个参数,基本能覆盖所有实际场景。如果参数更多,建议重构代码或使用自定义类型封装参数。

委托 vs 接口

虽然委托和接口都能实现回调机制,但它们适用于不同场景:

  • 委托适合单一方法的回调、事件处理、简单的策略模式
  • 接口适合定义一组相关方法的契约、需要多态性的场景

选择原则:如果只需要调用单个方法,委托更轻量;如果需要定义一组行为契约,接口更合适。

性能考虑

委托的性能通常很好,但有几点值得注意:

  1. 装箱开销:值类型的实例方法创建委托时会产生装箱
  2. 多播委托:调用链越长,性能开销越大
  3. 闭包捕获:Lambda 表达式捕获外部变量会产生额外的内存分配

对于性能敏感的场景,可以考虑对象池、避免不必要的委托创建、或使用函数指针(C# 9.0+)。

C#委托真的非常强大!!!它不仅是事件系统的基石,更是函数式编程、异步编程和 LINQ 的核心机制。

相关链接:

[1] C#中的委托和事件