C# 语言规范_版本5.0 (第15章 委托)

时间:2022-01-12 05:49:33

1. 委托

**(注:此章非常重要,特别是对于图形界面相关的区别于MFC和QT等的消息机制,委托是基石。)

委托是用来处理其他语言(如 C++、Pascal 和 Modula)需用函数指针来处理的情况的。不过与 C++ 函数指针不同,委托是完全面向对象的;另外,C++ 指针仅指向成员函数,而委托同时封装了对象实例和方法。

委托声明定义一个从 System.Delegate 类派生的类。委托实例封装了一个调用列表,该列表列出了一个或多个方法,每个方法称为一个可调用实体。对于实例方法,可调用实体由该方法和一个相关联的实例组成。对于静态方法,可调用实体仅由一个方法组成。用一个适当的参数集来调用一个委托实例,就是用此给定的参数集来调用该委托实例的每个可调用实体。

委托实例的一个有趣且有用的属性是:它不知道也不关心它所封装的方法所属的类;它所关心的仅限于这些方法必须与委托的类型兼容(第 15.1 节)。这使委托非常适合于“匿名”调用。

1.1 委托声明

delegate-declaration 是一种 type-declaration(第 9.6 节),它声明一个新的委托类型。

delegate-declaration:
attributesopt  
delegate-modifiersopt  
delegate  
return-type  
          identifier  variant-type-parameter-listopt  
          (  
formal-parameter-listopt  
)  
type-parameter-constraints-clausesopt   ;

delegate-modifiers:
delegate-modifier
delegate-modifiers   delegate-modifier

delegate-modifier:
new
public
protected
internal
private

同一修饰符在一个委托声明中多次出现属于编译时错误。

new 修饰符仅允许在其他类型中声明的委托上使用,在这种情况下该修饰符表示所声明的委托会隐藏具有相同名称的继承成员,详见第 10.3.4 节。

public、protected、internal
和 private 修饰符将控制委托类型的可访问性。根据委托声明所在的上下文,可能不允许使用其中某些修饰符(第 3.5.1 节)。

上述的语法产生式中,identifier 用于指定委托的类型名称。

可选的 formal-parameter-list用于指定委托的参数,而 return-type 则指定委托的返回类型。

可选的 variant-type-parameter-list(第 13.1.3 节)指定委托本身的类型形参。

委托类型的返回类型必须为 void 或输出安全(第 13.1.3.1 节)。

委托类型的所有形参类型都必须是输入安全的。此外,所有
out 或 ref 参数类型也必须是输出安全的。请注意,由于基础执行平台的限制,甚至 out 形参也必须是输入安全的。

C# 中的委托类型是名称等效的,而不是结构等效的。具体地说,对于两个委托类型,即使它们具有相同的参数列表和返回类型,仍被认为是不同的两个委托类型。但是,两个不同但结构上等效的委托类型的实例可能比较为相等(第 7.9.8 节)。

例如:

delegate int
D1(int i, double d);

class A
{
public static int M1(int a, double b)
{...}
}

class B
{
delegate int D2(int c, double d);

public static int M1(int f, double g) {...}

public static void M2(int k, double l) {...}

public static int M3(int g) {...}

public static void M4(int g) {...}
}

方法 A.M1 和 B.M1  与委托类型
D1 和 D2 都兼容,因为这两个方法的返回类型和参数列表相同;但是,这两个委托类型是两个不同的类型,因此这两个委托类型不可互换。方法 B.M2 和 B.M3、B.M4 与委托类型 D1 和 D2 不兼容,因为这两个方法的返回类型或参数列表不同。

与其他泛型类型声明一样,必须提供类型实参才能创建构造委托类型。构造委托类型的形参类型和返回类型是通过将委托声明中的每个类型形参替换为构造委托类型的对应类型实参来创建的。结果返回类型和形参类型用于确定哪些方法与构造委托类型兼容。例如:

delegate bool
Predicate<T>(T value);

class X
{
static bool F(int i) {...}

static bool G(string s) {...}
}

方法 X.F 与委托类型 Predicate<int> 兼容,方法 X.G 与委托类型 Predicate<string> 兼容。

声明一个委托类型的唯一方法是通过 delegate-declaration。委托类型是从 System.Delegate 派生的类类型。委托类型隐含为 sealed,所以不允许从一个委托类型派生任何类型。也不允许从 System.Delegate 派生非委托类类型。请注意:System.Delegate 本身不是委托类型;它是从中派生所有委托类型的类类型。

C# 提供了专门的语法用于委托类型的实例化和调用。除实例化外,所有可以应用于类或类实例的操作也可以相应地应用于委托类或委托实例。具体而言,可以通过通常的成员访问语法访问 System.Delegate 类型的成员。

委托实例所封装的方法集合称为调用列表。从某个方法创建一个委托实例时(第 15.2 节),该委托实例将封装此方法,此时,它的调用列表只包含一个“入口点”。但是,当组合两个非空委托实例时,它们的调用列表将连接在一起(按照左操作数在前、右操作数在后的顺序)以组成一个新的调用列表,其中包含两个或更多个“入口点”。

委托是使用二元 +(第 7.8.4 节)和 += 运算符(第 7.17.2 节)进行组合的。可以使用二元 -(第 7.8.5 节)和 -= 运算符(第 7.17.2 节)将一个委托从委托组合中移除。委托间还可以进行比较以确定它们是否相等(第 7.10.8 节)。

下面的示例演示多个委托的实例化及其相应的调用列表:

delegate void
D(int x);

class C
{
public static void M1(int i) {...}

public static void M2(int i) {...}

}

class Test
{
static void Main() {
     D cd1 = new D(C.M1);     // M1
     D cd2 = new D(C.M2);     // M2
     D cd3 = cd1 + cd2;       // M1 + M2
     D cd4 = cd3 + cd1;       // M1 + M2 + M1
     D cd5 = cd4 + cd3;       // M1 + M2 + M1 + M1 + M2
}

}

实例化 cd1 和 cd2 时,它们分别封装一个方法。实例化 cd3 时,它的调用列表有两个方法 M1 和 M2,而且顺序与此相同。cd4 的调用列表中依次包含 M1、M2 和 M1。最后,cd5 的调用列表中依次包含 M1、M2、M1、M1 和 M2。有关组合(以及移除)委托的更多示例,请参见第 15.4 节。

1.2 委托兼容性

方法或委托 M 可兼容 (compatible)委托类型 D,前提是以下所有条件都成立:

  • D 和 M 具有相同数量的形参,并且 D 中的每个形参都具有与 M 中对应形参相同的 ref 或 out 修饰符。
  • 对于每个值形参(没有 ref 或 out 修饰符的形参),存在从 D 中形参类型到 M 中对应形参类型的标识转换(第 6.1.1 节)或隐式引用转换(第 6.1.6 节)。
  • 对于每个 ref 或 out 参数,D 中的参数类型与 M 中的参数类型相同。
  • 存在从 M 的返回类型到 D 的返回类型的标识或隐式引用转换。

1.3 委托实例化

委托的实例通过 delegate-creation-expression(第 7.6.10.5 节)或到委托类型的转换进行创建。因此,新创建的委托实例将引用以下各项之一:

  • delegate-creation-expression 中引用的静态方法,或者
  • delegate-creation-expression 中引用的目标对象(此对象不能为 null)和实例方法,或者
  • 另一个委托。

例如:

delegate void
D(int x);

class C
{
public static void M1(int i) {...}
public void M2(int i) {...}
}

class Test
{
static void Main() {
     D cd1 = new D(C.M1);     // static method
     C t = new C();
     D cd2 = new D(t.M2);     // instance method
     D cd3 = new D(cd2);      // another delegate
}
}

委托实例一旦被实例化,它将始终引用同一目标对象和方法。记住,当组合两个委托或者从一个委托移除另一个时,将产生一个新的委托,该委托具有它自己的调用列表;被组合或移除的委托的调用列表将保持不变。

1.4 委托调用

C# 为调用委托提供了专门的语法。当调用非空的、调用列表仅包含一个入口点的委托实例时,它调用调用列表中的方法,委托调用所使用的参数和返回的值均与该方法的对应项相同。(有关委托调用的详细信息,请参见第 7.6.5.3 节。)如果在对这样的委托进行调用期间发生异常,而且没有在被调用的方法内捕捉到该异常,则会在调用该委托的方法内继续搜索与该异常对应的 catch 子句,就像调用该委托的方法直接调用了该委托所引用的方法一样。

如果一个委托实例的调用列表包含多个入口点,那么调用这样的委托实例就是按顺序同步地调用调用列表中所列的各个方法。以这种方式调用的每个方法都使用相同的参数集,即提供给委托实例的参数集。如果这样的委托调用包含引用参数(第 10.6.1.2 节),那么每个方法调用都将使用对同一变量的引用;这样,若调用列表中有某个方法对该变量进行了更改,则调用列表中排在该方法之后的所有方法都会见到此变更。如果委托调用包含输出参数或一个返回值,则它们的最终值就是调用列表中最后一个方法调用所产生的结果。

如果在处理此类委托的调用期间发生异常,而且没有在正被调用的方法内捕捉到该异常,则会在调用该委托的方法内继续搜索与该异常对应的 catch 子句,此时,调用列表中排在后面的任何方法将不会被调用。

试图调用其值为 null 的委托实例将导致 System.NullReferenceException 类型的异常。

下面的示例演示如何实例化、组合、移除和调用委托:

using System;

delegate void
D(int x);

class C
{
public static void M1(int i) {
     Console.WriteLine("C.M1: "
+ i);
}

public static void M2(int i) {
     Console.WriteLine("C.M2: "
+ i);
}

public void M3(int i) {
     Console.WriteLine("C.M3: "
+ i);
}
}

class Test
{
static void Main() {
     D cd1 = new D(C.M1);
     cd1(-1);             // call M1

D cd2 = new D(C.M2);
     cd2(-2);             // call M2

D cd3 = cd1 + cd2;
     cd3(10);             // call M1 then M2

cd3 += cd1;
     cd3(20);             // call M1, M2, then M1

C c = new C();
     D cd4 = new D(c.M3);
     cd3 += cd4;
     cd3(30);             // call M1, M2, M1, then M3

cd3 -= cd1;          //
remove last M1
     cd3(40);             // call M1, M2, then M3

cd3 -= cd4;
     cd3(50);             // call M1 then M2

cd3 -= cd2;
     cd3(60);             // call M1

cd3 -= cd2;          //
impossible removal is benign
     cd3(60);             // call M1

cd3 -= cd1;          //
invocation list is empty so cd3 is null

//     cd3(70);      // System.NullReferenceException thrown

cd3 -= cd1;          //
impossible removal is benign
}
}

如语句 cd3 += cd1; 中所演示,委托可以多次出现在一个调用列表中。这种情况下,它每出现一次,就会被调用一次。在这样的调用列表中,当移除委托时,实际上移除的是调用列表中最后出现的那个委托实例。

就在执行最后一条语句 cd3 -= cd1; 之前,委托 cd3 引用了一个空的调用列表。试图从空的列表中移除委托(或者从非空列表中移除表中没有的委托)不算是错误。

产生的输出为:

C.M1: -1
C.M2: -2
C.M1: 10
C.M2: 10
C.M1: 20
C.M2: 20
C.M1: 20
C.M1: 30
C.M2: 30
C.M1: 30
C.M3: 30
C.M1: 40
C.M2: 40
C.M3: 40
C.M1: 50
C.M2: 50
C.M1: 60
C.M1: 60