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

C# 语言规范_版本5.0 (第15章 委托)的更多相关文章

  1. C# 语言规范_版本5.0 (第2章 词法结构)

    1. 词法结构 1.1 程序 C# 程序 (program) 由一个或多个源文件 (source file) 组成,源文件的正式名称是编译单元 (compilation unit)(第 9.1 节). ...

  2. C# 语言规范_版本5.0 (第10章 类)

    1. 类 类是一种数据结构,它可以包含数据成员(常量和字段).函数成员(方法.属性.事件.索引器.运算符.实例构造函数.静态构造函数和析构函数)以及嵌套类型.类类型支持继承,继承是一种机制,它使派生类 ...

  3. C# 语言规范_版本5.0 (第7章 表达式)

    1. 表达式 表达式是一个运算符和操作数的序列.本章定义语法.操作数和运算符的计算顺序以及表达式的含义. 1.1 表达式的分类 一个表达式可归类为下列类别之一: 值.每个值都有关联的类型. 变量.每个 ...

  4. C# 语言规范_版本5.0 (第4章 类型)

    1. 类型 C# 语言的类型划分为两大类:值类型 (Value type) 和引用类型 (reference type).值类型和引用类型都可以为泛型类型 (generic type),泛型类型采用一 ...

  5. C# 语言规范_版本5.0 (第3章 基本概念)

    1. 基本概念 1.1 应用程序启动 具有入口点 (entry point) 的程序集称为应用程序 (application).应用程序运行时,将创建新的应用程序域 (application doma ...

  6. C# 语言规范_版本5.0 (第18章 不安全代码)

    1. 不安全代码 **(注:此章对于跨多语言编程开发非常重要,如遇异常无法完成跨语言,建议使用此种方式.) 如前面几章所定义,核心 C# 语言没有将指针列入它所支持的数据类型,从而与 C 和 C++ ...

  7. C# 语言规范_版本5.0 (第17章 特性)

    1. 特性 C# 语言的一个重要特征是使程序员能够为程序中定义的实体指定声明性信息.例如,类中方法的可访问性是通过使用 method-modifiers(public.protected.intern ...

  8. C# 语言规范_版本5.0 (第11章 结构)

    1. 结构 结构与类的相似之处在于,它们都表示可以包含数据成员和函数成员的数据结构.但是,与类不同,结构是一种值类型,并且不需要堆分配.结构类型的变量直接包含了该结构的数据,而类类型的变量所包含的只是 ...

  9. C# 语言规范_版本5.0 (第8章 语句)

    1. 语句 C# 提供各种语句.使用过 C 和 C++ 编程的开发人员熟悉其中大多数语句. statement: labeled-statement declaration-statement emb ...

随机推荐

  1. 自定义radio图标

    问题: 默认的radio控件不是很好看,我们能否自定义一个radio图标? 解决: 1.radio有input和lable两个标签. 2.<input>是前面的图标,选中后图标变化. 3. ...

  2. C语言的一些常见细节

    C语言的一些常见细节 对于C语言,不同的编译器采用了不同的实现,并且在不同平台上表现也不同.脱离具体环境探讨C的细节行为是没有意义的,以下是我所使用的环境,大部分内容都经过测试,且所有测试结果基于这个 ...

  3. 实现Launcher编辑模式(1) 壁纸更换

    Android Launcher分析和修改13——实现Launcher编辑模式(1) 壁纸更换 Posted on 2013-09-11 23:25 泡泡糖 阅读(212) 评论(3) 编辑 收藏 已 ...

  4. 一个使用MVC3+NHibernate “增删改查” 的项目

    一个使用MVC3+NHibernate “增删改查” 的项目  前言: 谈到NHibernate大伙并不陌生,搞Java的更是清楚,Hibernate是一个目前应用的最广泛的开放源代码的对象关系映射框 ...

  5. Myeclipse新建 配置Hibernate

    一.新建一个JAVA项目 二.选中新建的项目单击右键[Add Hibernate Capab-] 三.添加MyEclipse Hiberate Libaries(Hibernate 3.2) 单击[B ...

  6. NPinyin 中文转换拼音代码

    Mono 3.2 测试NPinyin 中文转换拼音代码   C#中文转换为拼音NPinyin代码  在Mono 3.2下运行正常,Spacebuilder 有使用到NPinyin组件,代码兼容性没有问 ...

  7. .net 配置文件 分析 EntityName 时出错

    今天用C#读写XML文档,总出现下面的错误: 分析 EntityName 时出错.行1,位置9. 出错地方的源程序为: //...... pathEle.InnerXml = reducedStr(v ...

  8. boost解析XML方法教程

    boost库在解析XML时具有良好的性能,可操作性也很强下地址有个简单的说明 http://blog.csdn.net/luopeiyuan1990/article/details/9445691 一 ...

  9. 揭开Html 标签的面纱,忘不了的html .

     Html :(Hypertext MarkupLanguage),是用于描述网页文档的一种标记语言,是一种标准,它通过标记符号来标记要显示的网页中的各个部分.其本身是一种文本文件,通过在文本文件中添 ...

  10. Ubuntu12.10 下搭建基于KVM-QEMU的虚拟机环境(八)

    Libvirt 是用c写的一个管理虚拟机及其资源(如网络.存储和外设等)的工具库,它不仅支持KVM/QEMU,它还支持xen,Vmware,OpenVZ和VirtualBox等其他HyperVisor ...