1. 委托
委托是用来处理其他语言(如 C++、Pascal 和 Modula)需用函数指针来处理的情况的。不过与 C++ 函数指针不同,委托是完全面向对象的;另外,C++ 指针仅指向成员函数,而委托同时封装了对象实例和方法。
委托声明定义一个从 System.Delegate 类派生的类。委托实例封装了一个调用列表,该列表列出了一个或多个方法,每个方法称为一个可调用实体。对于实例方法,可调用实体由该方法和一个相关联的实例组成。对于静态方法,可调用实体仅由一个方法组成。用一个适当的参数集来调用一个委托实例,就是用此给定的参数集来调用该委托实例的每个可调用实体。
委托实例的一个有趣且有用的属性是:它不知道也不关心它所封装的方法所属的类;它所关心的仅限于这些方法必须与委托的类型兼容(第 15.1 节)。这使委托非常适合于“匿名”调用。
1.1 委托声明
delegate-declaration 是一种 type-declaration(第 9.5 节),它声明一个新的委托类型。
delegate-declaration:
attributesopt delegate-modifiersopt delegate return-type identifier
( formal-parameter-listopt ) ;
delegate-modifiers:
delegate-modifier
delegate-modifiers delegate-modifier
delegate-modifier:
new
public
protected
internal
private
同一修饰符在一个委托声明中多次出现属于编译时错误。
new 修饰符仅允许在其他类型中声明的委托上使用,在这种情况下该修饰符表示所声明的委托会隐藏具有相同名称的继承成员,详见第 10.2.2 节。
public、protected、internal 和 private 修饰符控制委托类型的可访问性。根据委托声明所在的上下文,可能不允许使用其中某些修饰符(第 3.5.1 节)。
上述的语法产生式中,identifier 用于指定委托的类型名称。
可选的 formal-parameter-list 用于指定委托的参数,而 return-type 则指定委托的返回类型。如果下面两个条件都为真,则方法和委托类型是兼容的 (compatible):
· 它们具有相同的参数数目,并且类型相同,顺序相同,参数修饰符也相同。
· 它们的返回类型相同。
C# 中的委托类型是名称等效的,而不是结构等效的。具体地说,对于两个委托类型,即使它们具有相同的参数列表和返回类型,仍被认为是不同的两个委托类型。不过,这样两个彼此不同的但结构上又相同的委托类型,它们的实例在比较时可以认为是相等关系(第
例如:
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) {...}
}
委托类型 D1 和 D2 都与方法 A.M1 和 B.M1 兼容,这是因为它们具有相同的返回类型和参数列表;但是,这些委托类型是两个不同的类型,所以它们是不可互换的。委托类型 D1 和 D2 与方法 B.M2、B.M3 和 B.M4 不兼容,这是因为它们具有不同的返回类型或参数列表。
声明一个委托类型的唯一方法是通过 delegate-declaration。委托类型是从 System.Delegate 派生的类类型。委托类型隐含为 sealed,所以不允许从一个委托类型派生任何类型。也不允许从 System.Delegate 派生非委托类类型。请注意:System.Delegate 本身不是委托类型;它是从中派生所有委托类型的类类型。
C# 提供了专门的语法用于委托类型的实例化和调用。除实例化外,所有可以应用于类或类实例的操作也可以相应地应用于委托类或委托实例。具体而言,可以通过通常的成员访问语法访问 System.Delegate 类型的成员。
委托实例所封装的方法集合称为调用列表。从某个方法创建一个委托实例时(第 15.2 节),该委托实例将封装此方法,此时,它的调用列表只包含一个进入点。但是,当组合两个非空委托实例时,它们的调用列表被连接在一起(按照左操作数在先、右操作数在后的顺序)以组成一个新的调用列表,它包含了两个或更多个“进入点”。
委托是使用二元 +(第 7.7.4 节)和 += 运算符(第 7.13.2 节)来组合的。可以使用一元 -(第
7.7.5 节)和 -= 运算符(第 7.13.2 节)将一个委托从委托组合中移除。委托间还可以进行比较以确定它们是否相等(第 7.9.8 节)。
下面的示例演示多个委托的实例化及其相应的调用列表:
delegate void D(int x);
class C
{
public static void M1(int i) {...}
public static void M2(int i) {...}
}
class Test
{
static void
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.3 节。
1.2 委托实例化
委托的实例是由 delegate-creation-expression(第 7.5.10.3 节)创建的。因此,新创建的委托实例将引用以下各项之一:
· 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
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.3 委托调用
C# 为调用委托提供了专门的语法。当调用非空的、调用列表仅包含一个进入点的委托实例时,它调用调用列表中的方法,委托调用所使用的参数和返回的值均与该方法的对应项相同。(有关委托调用的详细信息,请参见第 7.5.5.2 节。)如果在对这样的委托进行调用期间发生异常,而且没有在被调用的方法内捕捉到该异常,则会在调用该委托的方法内继续搜索与该异常对应的 catch 子句,就像调用该委托的方法直接调用了该委托所引用的方法一样。
如果一个委托实例的调用列表包含多个进入点,那么调用这样的委托实例就是按顺序同步地调用调用列表中所列的各个方法。以这种方式调用的每个方法都使用相同的参数集,即提供给委托实例的参数集。如果这样的委托调用包含引用参数(第 10.5.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
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:
评论