`

详解C#中的Delegate

阅读更多

如果你想拿 C# 与其它“C家族”的语言做比较,C# 正有个不同寻常的特性,其在 C++ 或者 Java 里没有真正意义上的对应之物。


C# 是一个颇具争议的新兴语言,由 Microsoft 开发创造,以作为其 Visual Studio.NET 的基石,目前正处于第一个 Beta 版的发布阶段。C# 结合了源自 C++ 和 Java 的许多特性。Java 社群对 C# 主要的批评在于,其声称 C# 只是一个蹩脚的 Java 克隆版本 ——与其说它是语言创新的成果,倒不如说是一桩诉讼的结果。而在 C++ 社群里,主要的批评(也同时针对 Java)是,C# 只不过是另一个泛吹滥捧的私有语言(yet another over-hyped proprietary language)。

本文意在展示一种 C# 的语言特性,而在 C++ 或 Java 中都没有直接支持类似的特性。这就是 C# 的 delegate 型别,其运作近似于一种指向成员函数的指针。我认为,C# delegate 型别是经过深思熟虑的创新型语言特性,C++ 程序员(无论其对 C# 或者 Microsoft 有何想法)应该会对这个特性产生特殊的兴趣。

为了激发讨论,我将围绕一个 testHarness class 的设计来进行阐述。这个 testHarness class 能够让任何类别对 static 或 non-static 的 class methods 进行注册,以便后续予以执行。Delegate 型别正是实现 testHarness class 的核心。

C# 的 Delegate Type

Delegate 是一种函数指针,但与普通的函数指针相比,区别主要有三:

1) 一个 delegate object 一次可以搭载多个方法(methods)[译注1] ,而不是一次一个。当我们唤起一个搭载了多个方法(methods)的 delegate,所有方法以其“被搭载到 delegate object 的顺序”被依次唤起——稍候我们就来看看如何这样做。

2) 一个 delegate object 所搭载的方法(methods)并不需要属于同一个类别。一个 delegate object 所搭载的所有方法(methods)必须具有相同的原型和形式。然而,这些方法(methods)可以即有 static 也有 non-static,可以由一个或多个不同类别的成员组成。

3) 一个 delegate type 的声明在本质上是创建了一个新的 subtype instance,该 subtype 派生自 .NET library framework 的 abstract base classes DelegateMulticastDelegate , 它们提供一组 public methods 用以询访 delegate object 或其搭载的方法(methods)

声明 Delegate Type

一个 delegate type 的声明一般由四部分组成:(a) 访问级别;(b) 关键字 delegate;(c)返回型别,以及该 delegate type 所搭载之方法的声明形式(signature);(d) delegate type 的名称,被放置于返回型别和方法的声明形式(signature)之间。例如,下面声明了一个 public delegate type Action , 用来搭载“没有参数并具有 void 返回型别”的方法:

public delegate void Action();

一眼看去,这与函数定义惊人的相似;唯一的区别就是多了 delegate 关键字。增加该关键字的目的就在于:要通过关键字(keyword)——而非字元(token)——使普通的成员函数与其它形似的语法形式区别开来。这样 就有了 virtual ,static , 以及 delegate 用来区分各种函数和形似函数的语法形式。

如果一个 delegate type 一次只搭载单独一个方法(method),那它就可以搭载任意返回型别及形式的成员函数。然而,如果一个 delegate type 要同时搭载多个方法(methods),那么返回型别就必须是 void [译注2] 。 例如,Action 就可以用来搭载一个或者多个方法(method)。在 testHarness class 实现中,我们就将使用上述的 Action 声明。

定义 Delegate Handle

在 C# 中我们无法声明全局对象;每个对象定义必须是下述三种之一:局部对象;或者型别的对象成员;或者函数参数列表中的参数。现在我只向你展示 delegate type 的声明。之后我们再来看如何将其声明为类别中的成员。

C# 中的 delegate type 与 class, interface, 以及 array types 一样,属于 reference type。每个 reference type 被分为两部分:

  • 一个具名的 句柄 (named handle),由我们直接操纵;以及
  • 一个该句柄所属型别的不具名对象(unamed object),由我们通过句柄间接进行操纵。必须经由 new 显式的创建该对象。

定义 reference type 是一个“两步走”的过程。当我们写:

Action theAction;

的时候,theAction 代表“delegate type Action 之对象”的一个 handle(句柄),其本身并非 delegate object。缺省情况下,它被设为 null 。如果我们试图在对其赋值(译 注:assigned,即与相应型别的对象做attachment) 之前就使用它,会发生编译期错误。例如,语句:

theAction();

会唤起 theAction 所搭载的方法(method(s))。然而,除非它在定义之后、使用之前被无条件的赋值(译 注:assigned,即与相应型别的对象做attachment) ,否则该语句会引发编译期错误并印出相关信息。

为 Delegate Object 分配空间

在这一节中,为了以最小限度的涉及面继续进行阐述,我们需要访问一个静态方法(static method)和一个非静态方法(non-static method),就此我采用了一个 Announce class。该类别的 announceDate 静态方法(static method)以 long form 的形式(使用完整单字的冗长形式)打印当前的日期到标准输出设备:

Monday, February 26, 2001

非静态方法(non-static method) announceTimeshort form 的形式(较简短的表示形式)打印当前时间到标准输出设备:

00:58

前两个数字代表小时,从午夜零时开始计算,后两个数字代表分钟。Announce class 使用了由 .NET class framework 提供的 DateTime class。Announce 类别的定义如下所示。

public class Announce{   public static void announceDate()   {      DateTime dt = DateTime.Now;      Console.WriteLine( "Today''''s date is {0}",                         dt.ToLongDateString() );   }   public void announceTime()   {      DateTime dt = DateTime.Now;      Console.WriteLine( "The current time now is {0}",                         dt.ToShortTimeString() );   }}

要让 theAction 搭载上述方法,我们必须使用 new 表达式创建一个 Action delegate type(译注:即创建一个该类别的对象) 。要搭载静态 方法,则传入构造函数的引数由三部分组成:该方法 所属类别的名称;方法的名称;分隔两个名称用的 dot operator(.):

theAction = new Action( Announce.announceDate );

要搭载非静态方法,则传入构造函数的引数也由三部分组成:该方法所属的类别对象名称;方法的名称;分隔两个名称用的 dot operator(.):

Announce an = new Announce();theAction   = new Action( an.announceTime );

可以注意到, theAction 被直接赋值,事先没有做任何检查(比如,检查它是否已经指代一个堆中的对象,如果是,则先删除该对象)。在 C# 中,存在于 managed heap(受托管的堆)中的对象由运行期环境对其施以垃圾收集动作(garbage collected )。我们不需要显式的删除那些经由 new 表达式分配的对象。

在程序的 managed heap(受托管的堆)中,new 表达式既可以为独个对象做分配

HelloUser myProg = new HelloUser();

也可以为数组对象做分配

string [] messages = new string[ 4 ];

分配语句的形式为:型别的名称,后跟关键字 new ,后跟一对圆括弧(表示单个对象)或者方括号(表示数组对象)。(在 C# 语言设计中的一个普遍特征就是,坚持使用单一明晰的形式来区别不同的功用。)

如下述数组对象所示,当我们在 managed heap(受托管的堆)中为 reference type 分配了空间:

int [] fib = new int[6]{ 1,1,2,3,5,8 };

对象自动的维护“指向它的句柄(handles)”之数目。在这个例子中,被 fib 所指向的数组对象有一个关联的引用计数器被初始化为1。如果我们现在初始化另一个句柄,使其指向 fib 所指代的数组对象:

int [] notfib = fib;

这次初始化导致了对 fib 所指代数组对象的一次 shallow copy (浅层拷贝)。这就是说,notfib 现在也指向 fib 所指向的数组对象。该数组对象所关联的引用计数变成了2。

如果我们经由 notfib 修改了数组中某个元素,比如

notfib [ 0 ] = 0;

这个改变对于 fib 也是可见的。如果这种对同一个对象的多重访问方式并非所需,我们就需要编写代码,做一个 deep copy (深层拷贝)。例如,

// 分配另一个数组对象notfib = new int [6];// 从 notfib 的第0个元素开始,// 依次将 fib 中的元素拷贝到 notfib 中去。// 见注释 [2]

fib.CopyTo( notfib, 0 );

notfib 现在并不指代 fib 所指代的那个对象了。先前被它们两个同时指向的那个对象将其关联的引用计数减去1。notfib 所指代对象的初始引用计数为1。如果我们现在也将 fib 重新赋值为一个新的数组对象——例如,一个包含了Fibonacci数列前12个数值的数组:

fib = new int[12]{ 1,1,2,3,5,8,13,21,34,55,89,144 };

对于之前被 fib 所指代的那个数组对象,其现在的引用计数变成了0。在 managed heap(受托管的堆)中,当垃圾收集器(garbage collector)处于活动状态时,引用计数为0的对象被其作上删除标记。

定义 Class Properties

现在让我们将 delegate object 声明为 testHarness class 的一个私有静态private static )成员。例如 [3]

public class testHarness{   public delegate void    Action();   static private  Action  theAction;   // ...}

下一步我们要为这个 delegate 成员提供读写访问机制。在 C# 中,我们不要提供显式的内联方法(inline methods)用来读写非公有的数据成员。取而代之,我们为具名的属性 (named property )提供 getset 访问符(accessors)。下面是个简单的 delegate property。我们不妨将其称为 Tester

public class testHarness{   static public Action Tester   {      get{ return theAction; }      set{ Action = value; }   }   // ...}

Property(属性)既可以封装静态数据成员,也可以封装非静态数据成员。Tester 就是 delegate type Action 的一个 static property(静态属性)。(可以注意到。我们将 accessor 定义为一个代码区块。编译器内部由此产生 inline method。)

get 必须以 property(属性)的型别作为返回型别。在这个例子中,其直接返回所封装的对象。如果采用“缓式分配(lazy allocation)”,get 可以在初次被唤起的时候建构并存放好对象,以便后用。

类似的,如果我们希望 property(属性)能够支持写入型访问,我们就提供 set accessor。set 中的 value 是一个条件型关键字(conditional-keyword)。也就是说,value 仅在 set property 中具有预定义的含义(译注:也就是说,value 仅在 set 代码段中被看作一个关键字) : 其总是代表“该 property(属性)之型别”的对象。在我们的例子中,valueAction 型别的对象。在运行期间,其被绑定到赋值表达式的右侧。在下面的例子中,

Announce an = new Announce();testHarnes.Tester =     new testHarness.Action    ( an.announceTime );

set 以内联(inline)的方式被展开到 Tester 出现的地方。value 对象被设置为由 new 表达式返回的对象。

唤起 Delegate Object

如之前所见,要唤起由 delegate 所搭载的方法,我们对 delegate 施加 call operator(圆括弧对):

testHarness.Tester();

这一句唤起了Tester property 的 get accessor;get accessor返回 theAction delegate handle。如果 theAction 在此刻并未指向一个 delegate object,那么就会有异常被抛出。从类别外部实行唤起动作的规范做法(delegate-test-and-execute , 先实现代理,再测试,最后执行之)如下所示:

if ( testHarness.Tester != null )   testHarness.Tester();

对于 testHarness class,我们的方法只简单的封装这样的测试:

static public void run(){   if ( theAction != null )      theAction();}

关联多个 Delegate Objects

要让一个 delegate 搭载多个方法,我们主要使用 += operator 和 -= operator。例如,设想我们定义了一个 testHashtable class。在构造函数中,我们把各个关联的测试加入到 testHarness 中:

public class testHashtable{   public void test0();   public void test1();   testHashtable()   {      testHarness.Tester += new testHarness.Action( test0 );      testHarness.Tester += new testHarness.Action( test1 );   }   // ...}

同样,如果我们定义一个 testArrayList class,我们也在 default constructor 中加入关联的测试。可以注意到,这些方法是静态的。

public class testArrayList{   static public void testCapacity();   static public void testSearch();   static public void testSort();   testArrayList()   {      testHarness.Tester += new         testHarness.Action(testCapacity);      testHarness.Tester += new testHarness.Action(testSearch);      testHarness.Tester += new testHarness.Action(testSort);   }   // ...}

testHarness.run 方法被唤起时,通常我们并不知道 testHashtabletestArrayList 中哪一个的方法先被唤起;这取决于它们构造函数被唤起的顺序。但我们可以知道的是,对于每个类别,其方法被唤起的顺序就是方法被加入 delegate 的顺序。

Delegate Objects 与 Garbage Collection(垃圾收集)

考察下列局部作用域中的代码段:

{   Announce an = new Announce();   testHarness.Tester +=       new testHarness.Action      ( an.announceTime );}

当我们将一个非静态方法加入到 delegate object 中之后,该方法的地址,以及“用来唤起该方法,指向类别对象的句柄(handle)”都被存储起来。这导致该类别对象所关联的引用计数自动增加。

an 经由 new 表达式初始化之后,managed heap(受托管的堆)中的对象所关联的引用计数被初始化为1。当 an 被传给 delegate object 的构造函数之后,Announce 对象的引用计数增加到2。走出局部作用域之后,an 的生存期结束,该引用计数减回到1——delegate object还占用了一个。

好消息是,如果有一个 delegate 引用了某对象的一个方法,那么可以保证该对象会直到“delegate object 不再引用该方法”的时候才会被施以垃圾收集处理[4] 。 我们不用担心对象会在自己眼皮底下被贸然清理掉了。坏消息是,该对象将持续存在(译注:这可能是不必要的) ,直到 delegate object 不再引用其方法为止。可以使用 -= operator 从 delegate object 中移除该方法。例如下面修正版本的代码;在局部作用域中,announceTime 先被设置、执行,然后又从 delegate object 中被移除。

{   Announce an = new Announce();   Action act  = new testHarness.Action( an.announceTime );   testHarness.Tester += act;   testHarness.run();   testHarness

我们对于设计 testHashtable class 的初始想法是,实现一个析构函数用以移除在构造函数中加入的测试用方法。然而,C# 中的析构函数调用机制与 C++ 中的却不大相同[5] 。C# 的析构函数既不会因为对象生存期结束而跟着被唤起,也不会因为释放了对象最后一个引用句柄( reference handle)而被直接唤起。事实上,析构函数仅在垃圾收集器作垃圾收集时才被调用,而施行垃圾收集的时机一般是无法预料的,甚至可以根本就没施行垃圾收集。

C# 规定,资源去配动作被放进一个称为 Dispose 的方法中完成,用户可以直接调用该方法:

public void Dispose (){   testHarness.Tester -= new testHarness.Action( test0 );   testHarness.Tester -= new testHarness.Action( test1 );}

如果某类别定义了一个析构函数,其通常都会唤起 Dispose

访问底层的类别接口

让我们再回头看看先前的代码:

{   Announce an = new Announce();   Action act  =        new testHarness.Action       ( an.announceTime );   testHarness.Tester += act;   testHarness.run();   testHarness.Tester -= act;}

另一种实现方案是,先检查 Tester 当前是否已经搭载了其它方法,如果是,则保存当前的委托列表(delegation list),将 Tester 重置为 act ,然后调用 run ,最后将 Tester 恢复为原来的状态。

我们可以利用底层的 Delegate 类别接口来获知 delegate 实际搭载的方法数目。例如,

if ( testHarness.Tester != null &&     testHarnest.GetInvocationList().Length != 0 )   {      Action oldAct = testHarness.Tester;      testHarness.Tester = act;      testHarness.run();      testHarness.Tester = oldAct;   }   else { ... }

GetInvocationList 返回 Delegate class objects 数组,数组的每个元素即代表该 delegate 当前搭载的一个方法。Length 是底层 Array class 的一个 property(属性)。Array class 实现了 C# 内建数组型别的语义[6]

经由 Delegate class 的 Method property,我们可以获取被搭载方法的全部运行期信息。如果方法是非静态的,那么经由 Delegate class 的 Target property,我们更可以获取调用该方法之对象(译注:即该方法所属类别的那个对象) 的全部运行期信息。在下面例子中,Delegate 的 methods(方法) 和 properties(属性)用红色表示:

If (testHarness.Tester != null ){   Delegate [] methods = test.Tester.GetInvocationList()
;   foreach ( Delegate d in methods )   {      MethodInfo theFunction = d.Method
;      Type       theTarget   = d.Target.GetType()
;   // 好的:现在我们可以知道 delegate 所搭载方法的全部信息   }}

总结

希望本文能够引起你对 C# delegate type 的兴趣。我认为 delegate type 为 C# 提供了一种创新性的“pointer to class method(类别方法之指针)”机制。或许本文还引起了你对 C# 语言以及 .NET class framework 的兴趣。

A good starting page for technical resources is <http://www.microsoft.com/net/>. An informative news group with Microsoft developer input dealing with both .NET and C# is <http://discuss.develop.com/dotnet.html>. Of course, questions or comments on C# or this article can be addressed to me at stanleyl@you-niversity.com . Finally, C# is currently in the process of standardization. On October 31, 2000, Hewlett-Packard, Intel, and Microsoft jointly submitted a proposed C# draft standard to ECMA, an international standards body (ECMA TC39/TG2). The current draft standard and other documentation can be found at <http://www.ecma.ch>.

致谢

I would like to thank Josee Lajoie and Marc Briand for their thoughtful review of an earlier draft of this article. Their feedback has made this a significantly better article. I would also like to thank Caro Segal, Shimon Cohen, and Gabi Bayer of you-niversity.com for providing a safety.NET.

注释

[1] 对于 C++ 程序员来说,有两点值得一题:(a) 需要在对象的型别名称之后放一对圆括弧作为 default constructor,以及(b) 用于数组下标的方括号要放在型别与数组名称之间。

[2] C# 中内建的数组是一种由 .NET class library 提供的 Array class 之对象。Array class 的静态方法和非静态方法都可以被 C# 内建数组对象使用。CopyToArray 的一个非静态方法。

[3] 与 Java 一样,C# 中的成员声明包括其访问级别。缺省的访问级别是 private

[4] 类似的,C++ 标准要求,被引用的临时对象必须直到引用的生存期结束时才能够被销毁。

[5] 在内部实现中,析构函数甚至都不曾存在过。一个类别的析构函数会被转换成 virtual Finalize 方法。

[6] 在 C# 中,一个条件判别式的结果必须得到 Boolean 型别。对 Length 值的直接判别,如if(testHarness.Length) ,并不是合法的条件判断。整型值无法被隐式的转换为 Boolean 值。

Stanley B. Lippman is IT Program Chair with you-niversity.com , an interactive e-learning provider of technical courses on Patterns, C++, C#, Java, XML, ASP, and the .NET platform. Previously, Stan worked for over five years in Feature Animation both at the Disney and DreamWorks Animation Studios. He was the software Technical Director on the Firebird segment of Fantasia 2000. Prior to that, Stan worked for over a decade at Bell Laboratories. Stan is the author of C++ Primer, Essential C++, and Inside the C++ Object Model. He is currently at work on C# Primer for the DevelopMentor Book Series for Addison-Wesley. He may be reached at stanleyl@you-niversity.com .

译注

[译注1]在C#中,所谓“method(方法)”,其实就是指我们平常所理解的成员函数,其字面意义与“function(函数)”非常接近。

[译注2]作者是就前述的那个 delegate type Action 声明而有此言。就一般而言,只要多个方法(methods)的返回型别相同并且参数也相同,就可以被同一个 delegate type 搭载。

分享到:
评论

相关推荐

    c#的回调函数(delegate关键字)

    C#中的委托(Delegate) 在C#中,委托(Delegate)是一种特殊的类型,用于封装方法的引用。它可以将方法作为参数传递,类似于C++中的函数指针,但是委托是类型安全和可靠的。 在上面的代码中,我们定义了一个委托...

    C#窗体中Invoke和BeginInvoke方法详解

    ### C#窗体中Invoke和BeginInvoke方法详解 在探讨C#窗体中`Invoke`和`BeginInvoke`方法的使用及其重要性之前,我们首先需要理解.NET框架下的多线程与GUI操作的基本原则,以及为何这两者在跨线程更新GUI时不可或缺。...

    详解C#委托,事件与回调函数

    详解 C# 委托、事件与回调函数 委托是 C# 中一种非常重要的概念,它可以将方法作为参数传递给其他方法,也可以将方法作为返回值返回给其他方法。委托的定义是:delegate 返回类型 委托名(参数列表);例如:...

    详解C#中的委托

    在C#编程语言中,委托是一种强大的工具,它允许我们将方法作为参数传递给其他方法,或者存储和调用一组相关的方法。本文将深入探讨C#中的委托,解释其概念、使用场景以及如何声明和调用委托。 首先,理解委托的基本...

    C#事件(event)使用方法详解

    C#事件(event)使用方法详解 事件(event)是C#中的一种机制,允许对象在发生某些情况时通知其他对象。事件(event)使用方法详解将从概念、使用方法、事件编程的优点、事件与委托的关系、事件的实现等方面进行讲解。 ...

    详解c# 委托链

    在C#编程中,委托是一种强大的工具,它允许我们将方法作为参数传递,或者将多个方法链接在一起形成所谓的“委托链”。本文将深入探讨C#委托链的概念,如何创建和使用委托链,以及其背后的实现机制。 一、委托链的...

    C#编程语言详解C#编程语言详解

    在C#中,一个简单的"Hello, world"程序如下: ```csharp using System; class Program { static void Main() { Console.WriteLine("Hello, world!"); } } ``` 1.2 类型 C#是一种强类型语言,这意味着每个变量...

    C#委托详解:匿名委托,委托的应用(含源代码)

    例如,`C#委托详解.pdf` 可能详细解释了委托的基本概念、用法以及如何结合匿名委托实现某些功能。而`委托与匿名委托.zip`可能包含了一些示例项目,你可以运行并查看其工作原理。 总的来说,掌握C#中的委托和匿名...

    详解C#委托,事件,Observer设计模式

    ### 详解C#委托,事件,Observer设计模式 在C#编程中,了解并掌握委托、事件以及Observer设计模式对于构建高效、灵活的应用程序至关重要。本文将深入探讨这些概念,并通过具体的示例来帮助理解。 #### 1. 将方法...

    C# 中的委托和事件 详解

    ### C#中的委托和事件详解 #### 引言 委托和事件是.NET Framework中的核心概念之一,对于初学者来说可能有些难以理解。然而,一旦掌握,就能极大地提高编程效率及软件设计的灵活性。本文旨在逐步深入地介绍委托与...

    C#中的委托delegate用法的示例详解

    【C#中的委托delegate用法的示例详解】 在C#编程语言中,委托是类型安全的函数指针,允许我们将方法作为参数传递给其他方法,或者存储在类的字段中,以便稍后调用。这种特性使得委托成为C#中实现事件处理和回调机制...

    C#调用系统API详解

    C#调用系统API详解 C#调用系统API是指在C#语言中使用系统提供的API函数,以实现特定的功能。这其中涉及到多个知识点,包括声明及其参数、类型转换、成员的传递等。 1. 声明及其参数 在C#中,使用系统API需要首先...

    C# 的函数编程详解

    【C#函数编程详解】 函数式编程是一种编程范式,它强调使用函数作为核心构建块,将计算视为函数的组合,而不是状态的变化或指令的序列。近年来,由于其简洁、可读性强和易于并行处理的特点,函数式编程在IT行业中...

    C# 中的EventHandler实例详解

    在C#编程语言中,`EventHandler`是一种标准的事件处理委托类型,用于处理事件的触发。事件是对象间通信的一种方式,通常用在用户界面(UI)编程中,当某个操作发生时,如按钮点击,事件会被触发,然后执行相关的处理...

    C# API 函数详解

    C# API 函数详解 C#(发音为 "C sharp")是一种面向对象的编程语言,由微软开发,主要用于构建Windows应用程序以及Web和移动应用程序。它融入了.NET框架,允许开发者利用丰富的API来实现各种功能。API(Application...

    c# 中的委托 详细讲解了如何使用c#中的委托机制

    C#中的委托机制详解 委托是C#中的一个非常重要的概念,它可以让开发者定义一个方法签名,然后将其作为参数传递给其他方法,或者将其赋值给变量,以便在后续使用时更加灵活。下面我们将深入讲解委托的定义、使用委托...

    C# 委托和事件在.NET Framework详解

    在C#中,委托是通过使用delegate关键字来定义的。 2. 委托的应用场景 委托在.NET Framework中的应用非常广泛,例如在事件处理、回调函数和异步编程中。委托可以使得代码更加灵活和可扩展,提高开发效率和代码质量...

    c#语法详解c#语法详解c#语法详解

    这只是C#语法的冰山一角,还有许多其他关键概念,比如接口(interface)、继承(inheritance)、多态(polymorphism)、委托(delegate)、事件(event)、泛型(generics)、异步编程(async/await)等等。学习C#...

Global site tag (gtag.js) - Google Analytics