荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: lvyou (阿门【Amen】), 信区: DotNET
标  题: 介绍.NET中的委派(Delegates)
发信站: 荔园晨风BBS站 (Mon Dec 31 17:36:18 2001), 转信


介绍.NET中的委派(Delegates)

作者: supervisor 点击链接查看作者详细信息


    回调函数
    回调函数的确是至今为止最有用的编程机制之一。C运行时的qsort函数利用回调函数对
数组元素进行排序。在Windows中,回调函数更是窗

口过程,钩子过程,异步过程调用,以及目前Microsoft .NET框架所必需的,在整个回调过
程中自始至终地使用回调方法。人们可以注册回调

方法以获得加载/卸载通知,未处理异常通知,数据库/窗口状态修改通知,文件系统修改通
知,菜单项选择,完成的异步操作通知,过滤一组

条目等等。
    在C/C++中,一个函数的地址就是内存地址。这个地址不会附带任何其它赋加信息,如
函数的参数个数,参数类型,函数的返回值类型以及

这个函数的调用规范。简言之,C/C++回调函数不是类型安全的。
    在.NET框架中,回调函数所受到的重用与它在Windows非受控编程中一样。不同的是在
.NET框架中提供了一种叫委派(delegates)的类型

安全机制。我们先来研究一下委派的声明。下面的代码展示了如何声明,创建和使用委派:

    //
    using System;
    using System.WinForms; // 在beta2版本中为:System.Windows.Forms
    using System.IO;

    class Set {
    private Object[] items;

    public Set(Int32 numItems) {
    items = new Object[numItems];
    for (Int32 i = 0; i < numItems; i++)
    items[i] = i;
    }

    // 定义 Feedback,类型为delegate
    // (注意: 这个类型在Set类中是嵌套的)
    public delegate void Feedback(
    Object value, Int32 item, Int32 numItems);

    public void ProcessItems(Feedback feedback) {
    for (Int32 item = 0; item < items.Length; item++) {
    if (feedback != null) {
    // 一旦指定了回调,便调用它们
    feedback(items[item], item + 1, items.Length);
    }
    }
    }
    }


    class App {
    [STAThreadAttribute]
    static void Main() {
    StaticCallbacks();
    InstanceCallbacks();
    }

    static void StaticCallbacks() {
    // 创建一个Set对象,其中有五个项目
    Set setOfItems = new Set(5);

    // 处理项目,feedback=null
    setOfItems.ProcessItems(null);
    Console.WriteLine();

    // 处理项目,feedback=Console
    setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToConsole));
    Console.WriteLine();

    // 处理项目,feedback =MsgBox
    setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToMsgBox));
    Console.WriteLine();

    // 处理项目,feedback = console AND MsgBox
    Set.Feedback fb = null;
    fb += new Set.Feedback(App.FeedbackToConsole);
    fb += new Set.Feedback(App.FeedbackToMsgBox);
    setOfItems.ProcessItems(fb);
    Console.WriteLine();
    }

    static void FeedbackToConsole(
    Object value, Int32 item, Int32 numItems) {
    Console.WriteLine("Processing item {0} of {1}: {2}.",
    item, numItems, value);
    }

    static void FeedbackToMsgBox(
    Object value, Int32 item, Int32 numItems) {
    MessageBox.Show(String.Format("Processing item {0} of {1}: {2}.",
    item, numItems, value));
    }

    static void InstanceCallbacks() {
    //创建一个Set对象,其中有五个元素
    Set setOfItems = new Set(5);

    // 处理项目,feedback=File
    App appobj = new App();
    setOfItems.ProcessItems(new Set.Feedback(appobj.FeedbackToFile));
    Console.WriteLine();
    }

    void FeedbackToFile(
    Object value, Int32 item, Int32 numItems) {

    StreamWriter sw = new StreamWriter("Status", true);
    sw.WriteLine("Processing item {0} of {1}: {2}.",
    item, numItems, value);
    sw.Close();
    }
    }
    //

    注意代码最上面的Set类。假设这个类包含一组将被单独处理的项目。当你创建Set对象
时,将它要操纵的项目数传递给它的构造函数。然

后构造函数再创建对象(Objects)数组并初始化每一个整型值。
    另外,Set类定义了一个公共的委派,这个委派指出某个回调函数的签名。在这个例子
中,委派Feedback 确定了一个带三个参数的方法(

第一个参数为Object,第二和第三个参数都是Int32类型)并且返回void。在某种意义上,
委派很像C/C++中表示某个函数地址的类型定义(

typedef)。
    此外,Set类定义了一个公共方法:ProcessItems。这个方法有一个参数feedback——
一个对委派Feedback 对象的引用。ProcessItems迭

代遍历所有的数组元素,并且针对每一个元素调用回调方法(由feedback变量指定哪一个会
调方法),这个回调方法被传递,从而以不同的方

式处理回调方法所传递的项目值,项目数量以及数组中的元素数目。可以看出回调方法能以
它选择的任何方式处理每一个项目。
    使用委派调用静态方法
    StaticCallbacks方法示范了用各种不同方式的回调委派。这个方法首先构造一个Set对
象,告诉它对象创建有五个对象元素的数组。然后

调用ProcessItems,在第一个调用中,它的feedback参数为null。ProcessItems呈现一个方
法,这种方法为每一个Set操纵的项目实现某种动作

。在第一个例子中,因为feedback参数是null,在处理每一个项目时不调用任何回调方法。

    第二个例子中创建了一个新的Set.FeedBack委派对象,这个委派对象是一个方法包装器
,允许方法的调用是经由这个包装器间接调用。对

于类型FeedBack的构造器来说,方法的名字(App.FeedBackConsole)被作为构造器的参数
传递;这就表示方法被包装。然后,从new操作符返

回的引用被传到ProcessItems。现在,当执行ProcessItems时,它会调用App类型的
FeedbackToConsole方法处理集合中的每一个项目。

FeedbackToConsole简单地将一个串输出到控制台,表示哪个项目被处理了以及这个项目的
值是什么。
    第三个例子与第二个例子基本相同。唯一的差别是Feedback委派对象包装的是另一个方
法:App.FeedbackToMsgBox。这个方法建立一个串

,用这个串表示哪个项目被处理以及这个项目的值是什么。然后将这个串显示在一个信息框
中。
    第四个例子也是静态调用的最后一个例子示范了如何将委派链接在一起形成一个链。在
这个例子中,首先创键一个Feedback委派对象的引

用变量fb,并将它初始化为null。这个变量指向委派链表的头。Null值表示链表中没有节点
。然后,构造Feedback委派对象,由这个对象包装

对App FeedbackToConsole方法的调用。C#中,+=操作符被用于将对象添加到fb引用的链表
中。Fb此时指向链表的头。
    最后,构造另一个Feedback委派对象,由这个对象包装对App FeedbackToMsgBox方法的
调用。C#中的+=操作符又一次被用于将对象添加到

fb引用的链表中,并且fb被新的链表的头更新。现在,当执行ProcessItems时,传递给它的
是Feedback委派链表的头指针。在ProcessItems内

部,调用回调方法的代码行实际上终止调用所有的在链表中由委派对象包装的回调方法。也
就是说,对于被迭代的每一个项目,都会调用

FeedbackToConsole,接着马上调用FeedbackToMsgBox。在后续文章中我将详细讨论委派链
的处理机制。
    有一点很重要,那就是在这个例子中每件事情都是类型安全的,例如,当构造
Feedback委派对象时,编译器保证App的FeedbackToConsole

和FeedbackToMsgBox方法都具备确切的原型,像由Feedback委派定义的一样。既两个方法必
须有三个参数(Object,Int32和Int32),并且两

个方法必须有相同的返回类型(void)。如果方法原型不匹配,则编译器将发出下面的出错
信息:“error CS0123:The signature of method

'App.FeedbackToMsgBox()' does not match this delegate
    type。”——意思是App.FeedbackToMsgBox()方法的签名与委派的类型不匹配。
    调用实例方法
    前面我们讨论了如何使用委派调用静态方法。但是委派还能被用于调用特定对象的实例
方法。在调用实例方法时,委派需要知道这个它要

用方法操作的对象的实例。
    为了理解实例方法的回调机制,让我们回头看看前面代码中的InstanceCallbacks方法
。这段代码与静态方法的情形极其相似。注意在Set

对象被创建之后,App对象被创建。这个App对象仅仅是创建而已,处于示例目的没有其它内
容。当新的Feedback委派对象被创建的时候,它的

构造齐备传到appobj.FeedbackToFile。这将导致这个委派包装对FeedbackToFile方法的引
用,FeedbackToFile是个实例方法(非静态)。当这

个实例方法被调用时,由appobj引用的对象被操作(作为隐藏传递参数)。
FeedbackToFile方法的作用有点像FeedbackToConsole 和

FeedbackToMsgBox,不同的是它打开一个文件并将处理的项目串添加到文件尾。

    揭开委派的神秘面纱
    从表面上看,委派好像很容易使用:用C#委派关键字定义,用类似new操作符的方式构
造它们的实例, 用类似方法调用的语法调用回调方

法(不同的是不使用方法名,而是使用指代委派对象的变量)。
    然而,委派的实际运行机制要比前述例子中所描述的过程要复杂的多。编译器和公共语
言运行时(CLR)在幕后所做的许多处理隐藏了这些

复杂性,在这一部分中,我们将集中精力来讨论编译器和CLR是如何协同工作实现委派机制
的。这些知识将极大地丰富你对委派的理解并且这些

知识将告诉你如何有效地使用它们。我们还将涉及到一些在编程中能用到的委派的附加特性

    我们还是从下面这行代码开始:
    public delegate void Feedback(
    Object value, Int32 item, Int32 numItems);

    当编译器看到之一行代码时,它会产生一个完整的类定义,这个定义的代码会像下面这
个样子:
    //
    public class Feedback : System.MulticastDelegate {
    // 构造器
    public Feedback(Object target, Int32 methodPtr);

    // 方法与源代码描述的原型相同
    public void virtual Invoke(
    Object value, Int32 item, Int32 numItems);

    // 方法允许被异步回调,后继文章将讨论这些方法
    public virtual IAsyncResult BeginInvoke(
    Object value, Int32 item, Int32 numItems,
    AsyncCallback callback, Object object);
    public virtual void EndInvoke(IAsyncResult result);
    }
    //

    事实上,通过使用ILDasm.exe程序检查结果模块(如图三),你能发现编译器确实自动
产生了这个类。

    图三 检查编译器产生的类
    在这个例子中,编译器已经定义了一个叫Feedback的从System.MulticastDelegate类型
派生的类,它是在框架类库(Framework Class

Library)中定义的。要知道,所有委派类型都是从MulticastDelegate派生出来的。在这个
例子中,Feedback类是公共(public)类型的,因

为在源代码中它的类型被定义为public。如果用私有(private)或者受保护的(
protected)类型定义,则由编译器产生的Feedback类也将是

私有或者受保护的类型。你应该注意到委派类可能会在某个类中定义(如例子中Feedback就
是在Set类中定义的);委派也可能在被定义为全局

型。从本质上说,可以将委派看成是类,可以在定义类的任何地方定义委派。
    因为所有的委派都派生于MulticastDelegate,它们继承了MulticastDelegate的域,属
性和方法。在所有这些成员中,你要特别注意三个

私有(private)域:
    用于委派类型的私有域:
    域 类型 描述
    _target System.Object 指回调函数被调用时应该操作的对象。用于实例方法回调
    _methodPtr System.Int32 内部整型,CLR用它来标示被回调的方法
    _prev System.MulticastDelegate 指另一个委派对象,通常为null

    所有的委派都有代两个参数的构造器:一个参数是对象引用,一个是指代回调方法的整
型。但是,如果你检查源代码,就会发现明白诸如

App.FeedbackToConsole 或 appobj.FeedbackToFile的传递使用值进行的。你的敏感会告诉
你这个代码不能编译!
    然而,编译器知道某个委派被创建,同时编译器解析源代码以决定引用哪个对象和方法
。对象引用被传递为目标参数,并且用某个特定的

Int32值(从某个MethodDef或MethodRef元数据符号获得)标示的方法被传递为methodPtr参
数。对于静态方法,null被传递为目标参数。在构

造器内部,这两个参数被存储在它们对应的私有(private)域中。
    另外,构造器将这个域置为null。这个域被用来创建一个MulticastDelegate对象链表
。现在我们暂时忽略_prev域,在后续文章中将会详

细讨论有关它的内容。
    每一个委派对象实际上就是一个方法包装器,当方法被调用时,受作用的对象被操作。
MulticastDelegate类定义两个只读公共实例属性:

Target和Method。给定一个委派对象引用,你就可以查询到它的这些属性。如果方法被回调
,Target属性返回一个对将要操作的对象的引用。

如果方法是静态的,则Target返回null。Method属性返回标示回调方法的System.
Reflection.MethodInfo对象。
    你可以用几种方式使用这些信息。一种方式是检查是否某个委派对象引用特定类型的实
例方法:
    //
    Boolean DelegateRefersToInstanceMethodOfType(
    MulticastDelegate d, Type type) {

    return((d.Target != null) && d.Target.GetType == type);
    }
    //

    你还应该编写代码检查是否回调方法由专门的名字(如FeedbackToMsgBox):
    //
    Boolean DelegateRefersToMethodOfName(
    MulticastDelegate d, String methodName) {

    return(d.Method.Name == methodName);
    }
    //

    现在你知道了如何构造委派对象,下面让我们来谈谈回调方法是如何被调用的。为方便
起见,我们还是使用
    Set类中的ProcessItems:
    //
    public void ProcessItems(Feedback feedback) {
    for (Int32 item = 1; item <= items.Length; item++) {
    if (feedback != null) {
    // 如果指定任何回调,则调用它们
    feedback(items[item], item, items.Length);
    }
    }
    }
    //

    注释行下面的那一行代码就是调用回调方法。仔细看看代码,它调用feedback函数并传
递三个参数。但是feedback是不存在的。再一次指

出,编译器知道feedback是个引用某个委派对象的变量,并且编译器会产生实际的代码来调
用委派对象的Invoke方法。换句话说,编译器看到

下面这行代码后:
    feedback(items[item], item, items.Length);

    编译器产生的结果与下面这行源代码产生的结果一样:
    feedback.Invoke(items[item], item, items.Length);

    事实上,通过使用ILDasm.exe程序检查ProcessItems代码结果(如图五),你能发现这
一点。

    图五 分解后的 Set类的 ProcessItems
    图五显示了用于Set类型中ProcessItems方法的微软中介语言。其中红色的箭头指向的
指令调用Set.Feedback的Invoke方法。如果你修改源

代码来显式调用Invoke方法,C#编译器报错,出错信息为:“error CS1533: Invoke
cannot be called directly on a delegate”——意思

是Invoke不能针对某个委派被直接调用。C#不允许你显式调用Invoke(但是,但别的编译器
可以)。
    你会想起当编译器定义Feedback类的时候,它也定义了Invoke方法。当Invoke被调用时
,它使用私有的_target和_methodPtr域来为特定对

象调用希望的方法。注意Invoke方法的签名与委派的签名要完全匹配。也就是说,
Feedback委派带三个参数并返回void,那么Invoke方法也必

须带三个相同的参数并返回void。

    结论
    本文讨论了有关委派的基本概念。根据本文目前所讨论的内容,现在你应该能够创建并
使用它们。在后继文章中,我将解释链表中的委派

链,以及一些有关MulticastDelegate的附加方法,System.Delegate类型和事件等......。
等着我的好消息吧。

    本文是系列文章中的第四篇,前面三篇文章分别是:


转自站: aspcool
原作者: 赵湘宁




--
  ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬◤      ◣ ╬
  ╬╬╬烛光开始闪烁╬流下最后的眼泪╬╬╬╬╬╬╬╬╬╬╬╬╬ ◤◤      ▉╬
  ╬╬╬╬╬╬╬╬╬一种回忆╬一个思索╬╬╬╬╬╬╬╬╬╬╬Δ ◥ ㄧ◣   ▊╬
  ╬╬╬╬╬╬剩下一片漫漫的黑╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▊╬   ∥ ◥ ▋╬
  ╬╬╬╬╬╬╬╬╬╬╬╬╬是渐渐觉醒的灵魂╬╬╬╬╬╬╬╬▊╬ ◣∥   ◤ ╬
  ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▆▁▁◢◤ ◥╬╬

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.55.60]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店