C# 语言引用类型之委托

委托的定义

委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。你可以通过委托实例调用方法。

委托用于将方法作为参数传递给其他方法。事件处理程序就是通过委托调用的方法。你可以创建一个自定义方法,当发生特定事件时,某个类(如 Windows 控件)就可以调用你的方法。下面的示例演示了一个委托的声明:

  1. public delegate int PerformCalculation(int x, int y);

可将任何可访问类或结构中与委托类型匹配的任何方法分配给委托。该方法可以是静态方法,也可以是实例方法。此灵活性意味着你可以通过编程方式来更改方法调用,还可以向现有类中插入新代码。将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。

注意:在方法重载的上下文中,方法的签名不包括返回值。但在委托的上下文中,签名包括返回值。换句话说,方法和委托必须具有相同的返回类型。

委托的属性

委托具有以下属性:

  • 委托类似于C++ 函数指针,但委托完全面向对象,不像 C++ 指针会记住函数,委托会同时封装对象实例和方法。

  • 委托允许将方法作为参数进行传递。

  • 委托可用于定义回调方法。

  • 委托可以链接在一起,例如,可以对一个事件调用多个方法。

  • 方法不必与委托类型完全匹配。

  • 使用 Lambda 表达式可以更简练地编写内联代码块。Lambda 表达式(在某些上下文中)可编译为委托类型。

委托的使用与多播委托

委托类似于 CC++ 中的函数指针。与 C 函数指针不同的是,委托是面向对象的、类型安全可靠的。委托的类型由委托的名称确定。以下示例声明名为 Del 的委托,该委托可以封装采用字符串作为参数并返回 void 的方法:

  1. public delegate void Del(string message);

委托对象通常通过提供委托将封装的方法的名称或使用匿名函数构造。对委托进行实例化后,委托会将对其进行的方法调用传递到该方法。调用方传递到委托的参数将传递到该方法,并且委托会将方法的返回值(如果有)返回到调用方。这被称为调用委托。实例化的委托可以按封装的方法本身进行调用。例如:

  1. // 创建一个委托方法
  2. public static void DelegateMethod(string message)
  3. {
  4. Console.WriteLine(message);
  5. }
  6. // 实例化委托
  7. Del handler = DelegateMethod;
  8. // 调用委托
  9. handler("Hello World");

委托类型派生自 .NET 中的 Delegate 类。委托类型是密封的,它们不能派生自 Delegate,也不能从其派生出自定义类。由于实例化的委托是一个对象,因此可以作为参数传递或分配给一个属性。这允许方法接受委托作为参数并在稍后调用委托。这被称为异步回调,是在长进程完成时通知调用方的常用方法。当以这种方式使用委托时,使用委托的代码不需要知道要使用的实现方法。功能类似于封装接口提供的功能。

回调的另一个常见用途是定义自定义比较方法并将该委托传递到短方法。它允许调用方的代码成为排序算法的一部分。以下示例方法使用 Del 类型作为参数:

  1. public static void MethodWithCallback(int param1, int param2, Del callback)
  2. {
  3. callback("The number is: " + (param1 + param2).ToString());
  4. }

然后,你可以将上面创建的委托传递到该方法:

  1. MethodWithCallback(1, 2, handler);
  2. // 输出
  3. The number is: 3

上面的例子,以抽象方式使用委托时,MethodWithCallback 不需要直接调用控制台。MethodWithCallback 的作用是简单准备字符串并将字符串传递到其他方法。

当委托构造为封装实例方法时,委托将同时引用实例和方法。委托不知道除其所封装方法以外的实例类型,因此委托可以引用任何类型的对象,只要该对象上有与委托签名匹配的方法。当委托构造为封装静态方法时,委托仅引用方法。 请考虑以下声明:

  1. public class MethodClass
  2. {
  3. public void Method1(string message) { }
  4. public void Method2(string message) { }
  5. }

加上之前显示的静态 DelegateMethod ,我们现在已有三个 Del 实例可以封装的方法。调用时,委托可以调用多个方法。这被称为多播委托。若要向委托的方法列表(调用列表)添加其他方法,只需使用加法运算符或加法赋值运算符(“+”“+=”)添加两个委托。例如:

  1. var obj = new MethodClass();
  2. Del d1 = obj.Method1;
  3. Del d2 = obj.Method2;
  4. Del d3 = DelegateMethod;
  5. // 两种类型的赋值方法都有效
  6. Del allMethodsDelegate = d1 + d2;
  7. allMethodsDelegate += d3;

此时, allMethodsDelegate 的调用列表中包含三个方法,分别为 Method1Method2DelegateMethod 。 原有的三个委托( d1d2d3 )保持不变。调用 allMethodsDelegate 时,将按顺序调用所有三个方法。

如果委托使用引用参数,引用将按相反的顺序传递到这三个方法,并且一种方法进行的任何更改都将影响到另一种方法上的参数。当方法引发未在方法内捕获到的异常时,该异常将传递到委托的调用方,并且不会调用调用列表中的后续方法。如果委托具有返回值和/或输出参数,它将返回上次调用方法的返回值和参数。若要删除调用列表中的方法,请使用减法运算符或减法赋值运算符( --= )。 例如:

  1. // 删除Method1
  2. allMethodsDelegate -= d1;
  3. // 复制 AllMethodsDelegate,同时删除d2
  4. Del oneMethodDelegate = allMethodsDelegate - d2;

由于委托类型派生自 System.Delegate ,因此可以在委托上调用该类定义的方法和属性。例如,若要查询委托调用列表中方法的数量,你可以编写:

  1. int invocationCount = d1.GetInvocationList().GetLength(0);

调用列表中具有多个方法的委托派生自 MulticastDelegate,该类属于 System.Delegate 的子类。 由于这两个类都支持 GetInvocationList ,因此在其他情况下,上述代码也将产生作用。

多播委托广泛用于事件处理中。事件源对象将事件通知发送到已注册接收该事件的接收方对象。若要注册一个事件,接收方需要创建用于处理该事件的方法,然后为该方法创建委托并将委托传递到事件源。事件发生时,源调用委托。然后,委托将对接收方调用事件处理方法,从而提供事件数据。 给定事件的委托类型由事件源确定。

在编译时比较分配的两个不同类型的委托将导致编译错误。如果委托实例是静态的 System.Delegate 类型,则允许比较,但在运行时将返回 false。 例如:

  1. delegate void Delegate1();
  2. delegate void Delegate2();
  3. static void method(Delegate1 d, Delegate2 e, System.Delegate f)
  4. {
  5. // 编译错误
  6. // Console.WriteLine(d == e);
  7. // 如果运行时类型为 f 则编译正常
  8. Console.WriteLine(d == f);
  9. }

强类型委托

抽象的 Delegate 类提供用于松散耦合和调用函数的基础结构。使用 delegate 关键字并定义具体的委托类型时,编译器将生成这些方法。但是,在实际开发过程中,我们可能会创建一个个不同的委托类型。一段时间后此操作可能变得繁琐。因为,每个新功能都需要新的委托类型。所以,我们可能需要新的方法去声明自定义委托。

Action
  1. public delegate void Action();
  2. public delegate void Action<T>(T arg);
  3. public delegate void Action<T1, T2>(T1 arg1, T2 arg2);

Action委托非泛型版本是一个无参、无返回值的委托类型。Action<T...> 委托的泛型版本是一个有参、无返回值的泛型委托类型,重载版本多达 16 种,如

  1. Action
  2. Action<T1>
  3. Action<T1...>
  4. Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>

Action的使用也非常简单:

  1. public Action<string> action2;
  2. public void method2(string name)
  3. {
  4. Console.WriteLine(name);
  5. }
  6. action1 = method1;
  7. action1("赵一");
Func

FuncAction 不同的是,Func 有返回值,且必须有返回值。

  1. Func //会出现编译错误,因为没有返回参数
  2. Func<T1>
  3. Func<T1...>
  4. Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>

Func使用方法与 Action 一样,不同的是 Func 的最后一个类型默认返回值类型:

  1. public Func<string> f1; // 无参,有类型为string返回值的委托
  2. public Func<string ,int> f1; // 有一个参数,且有类型为int返回值的委托
  3. public Func<string ,... ,bool> f1; // 有参,且有类型为bool返回值的委托