函数指针的进化论(3)
函数指针
Delegate
C# 也支持多态与反射,但是 C# 却是使用 delegate 来实现多线程和回调 (而不使用多态与反射)。delegate 是函数指针的改良品种。delegate 的效率应该比多态稍差,但是用起来更方便,且允许使用静态方法。
C# 编译器对 delegate 以及 event 提供了大量的语法甜头 (syntactic sugar),这些语法甜头并不符合一般观念中的程序语法,所以往往让许多初学者丈二金刚摸不着头脑。后面会陆续揭露 C# 编译器的这些内幕。
C# 不支持函数指针,所以不能使用下面的语法:
void (*pFnc)(int, double);
必须改用下面的语法来宣告 delegate:
delegate void MyDelegate(int p1, double p2);
而上面的语法,等于下面的效果:
//版本一
class MyDelegate : System.MulticastDelegate { public MyDelegate(Object target, System.IntPtr) { ... } public void virtual Invoke(int p1, double p2) { ... } public virtual IasyncResult BeginInvoke(...) { ... } public virtual void EndInvoke(...) { ... }}
其实,也可以是:
//版本二
class MyDelegate : System.MulticastDelegate { public MyDelegate(Object target, System.IntPtr){ ... } // IntPtr 和 pointer 无关,是 native int 的意思
public void virtual Invoke(int p1, double p2) { ... }}
为了简单起见,我们只讨论版本二。请注意,不管版本一与版本二,都无法编译成功,因为 C# 语言规定:只有 C# 编译器可以直接制造出继承自 MulticastDelegate 的类别,编程员不可以在 C# 原代码中定义 MulticastDelegate 的衍生类别。换句话说,这样的语法甜头是强制的,非用不可,别无选择。
补充说明,MulticastDelegate 继承自 Delegate。微软原本的意思是让 Delegate 的衍生类别只能包装一个方法,MulticastDelegate 的衍生类别可以包装多个方法。但是后来发现这样的设计有相当多缺点,所以干脆让所有的 delegate 都继承自 MulticastDelegate。由于这样重大的设计变更来得太晚,所以微软不敢全面调整 .NET Framework,怕会因此出现 bug,所以没有更动原先的链接库,只有更动编译器和文件。读者可能会认为,为何不用 .NET 特有的 side-by-side execution 方式 (用来解决 DLL Hell),同时执行两个不同版本的 dll?我认为,问题之一出在 MulticastDelegate 与 Delegate 是属于 mscorlib.dll,这是绝对不能使用 side-by-side execution 的 dll。目前 (1.0 与 1.1) 虽然 MulticastDelegate 与 Delegate 都还存在,但是在未来的 .NET 版本可就难说了。
编译器帮我们产生的构造函数需要两个参数,第一个是方法所属的对象,第二个是方法在「Method」metadata table 中的位置。编程员当然不会知道这个位置是几号 (但是编译器知道),所以编程员无法直接使用此构造函数。事实上,产生 delegate 对象的过程中充满离奇,有许多语法甜头,下面会一一解释。
你可以用下面的方式,来产生一个非静态方法的 delegate:
new MyDelegate(myObject.MyNonStaticMethod);
编译器会自动调用 MyDelegate 构造函数,第一个参数是 myObject,第二个参数是 MyNonStaticMethod 方法在「Method」metadata table中的位置。
你也可以用下面的方式,来产生一个静态方法的 delegate:
new MyDelegate(MyClass.MyStaticMethod);
编译器会自动调用 MyDelegate 构造函数,第一个参数是 null,第二个参数是 MyStaticMethod 方法在 「Method」metadata table中的位置。
注意:不管是不是 static 方法,都必须符合 MyDelegate 的 signature (参数和返回值的型态),否则编译会失败。
下面有更怪的例子:
MyDelegate md = null;md += new MyDelegate(MyClass.MyStaticMethod);
第一次看到这样的程序代码,许多人都会吓了一跳:md 是 null,怎么可以使用 +=?这会不会导致 System.NullReferenceException?事实上,这样的写法,编译之后会变成:
MyDelegate md = null;md = System.Delegate.Combine(md, new MyDelegate(MyClass.MyStaticMethod));
Combine() 是 System.Delegate 所提供的静态方法,目的在于将第二个 Delegate 结合到第一个 Delegate 中,传回此一新的 Delegate;如果第一个 Delegate 为 null,则直接传回第二个 Delegate。
类似地,下面的程序:
md -= new MyDelegate(MyClass.MyStaticMethod);
编译之后会变成:
md = System.Delegate.Remove(md, new MyDelegate(MyClass.MyStaticMethod));
Remove() 是 System.Delegate 所提供的静态方法,目的在于将第二个 Delegate 自第一个 Delegate 中移除,并传回此一新的 Delegate。
稍早提到下面的定义:
delegate void MyDelegate(int p1, double p2);
会造成编译器会自动产生下面的定义。
class MyDelegate : System.MulticastDelegate { public MyDelegate(Object target, System.IntPtr){ ... } // IntPtr 和 pointer 无关,是 native int 的意思
public void virtual Invoke(int p1, double p2) { ... }}
现在我们把焦点集中在 Invoke() 上,此方法的参数和返回值型态一定会和 delegate 相同,以此例来说,方法参数必须是 int,double,而传出值必须是 void。
如何调用 delegate?相当简单,请看下面的例子:
MyDelegate d = new MyDelegate(MyClass.MyStaticMethod);d(1, 3.4);
delegate 其实还有许多有趣的主题,包括 System.Reflection.RuntimeMethodInfo 类别做了哪些事 (这个类别是 Undocumented,.NET 1.0 文件中没有说明)、多个 delegate 如何串接、delegate 如何和反射机制合作......等,因为篇幅有限,我都不在本文章说明,请感兴趣的读者自行研究这些主题。
C# 的多线程
传统的多线程使用函数指针当参数,C# 利用 delegate 来取代函数指针,所以当然也将 delegate 用在多线程上。下面是一个 C# 多线程的例子:
using System;using System.Threading;class SimpleThreadApp { public static void WorkerThreadMethod() { // ... } public static void Main() { ThreadStart woker = new ThreadStart(WorkerThreadMethod); Thread t = new Thread(worker); t.start(); }}
ThreadStart 是一个 delegate,由 System.Threading 所提供。这个程序应该不难理解,所以我不再解释。
C# 的回调
对于 C# 来说,事件来源可以使用下面的方式来定义:
public class YourButton { public YourDelegate Click; // ...}
这么一来,外面的程序如果想要注册,用法如下:
yourButton.Click += new YourDelegate(MyClass.MyStaticMethod);
在 YourButton 类别定义「内」,如果想通知所有的事件倾听者,只要用下面的程序代码即可:
Click();
糟糕的是,连在 YourButton 类别定义「外」,也可以使用下面的方式,来产生通知,这样子会违反对象导向的封装精神。
yourButton.Click();
所以显然我们应该将 YourButton 内的 Click 由 public 改成 private:
public class YourButton { private YourDelegate Click; // ...}
但是这样却造成外面的程序无法向 YourButton 注册,所以我们再将程序改成下面的模样:
//作法一
public class YourButton { private YourDelegate Click; public void add_Click(YourDelegate d) { Click += d; } public void remove_Click(YourDelegate d) { Click -= d; } // ...}
几乎大家都有这样的需求,所以 C# 编译器于是又提供了一个语法甜头 (利用 event 关键词),只要写出下面 (作法二) 的程序,编译之后的结果就和上面 (作法一) 一样:
//作法二
public class YourButton { public event YourDelegate Click; // ...}
或者你想要自行提供 add 和 remove 内的程序代码也成 (可能是为了提供 side-effect 程序代码),如下所示 (有点类似 property 的语法):
//作法三
public class YourButton { private YourDelegate _Click; public event YourDelegate Click { add { // .. side-effect code here, if any Click += value; // .. side-effect code here, if any } remove { // .. side-effect code here, if any Click -= value; // .. side-effect code here, if any } } // ...}
为何用作法三,不用作法一,因为作法三有使用 event 关键词,只要有使用 event 关键词 (包括作法二),就会使得编译器将它记录在「Event」Metadata Table 内。有没有纪录这个对于执行时的毫无影响,但是可以帮助编译器等工具软件判读,来简化原代码。例如,使用作法一,无法用下面的方式来注册以及取消注册。
yourButton.Click += new YourDelegate(MyClass.MyStaticMethod);yourButton.Click -= new YourDelegate(MyClass.MyStaticMethod);
但是,使用作法二和三,则可以用这种方式来注册以及取消注册。因为编译器从「Event」Metadata Table 内发现 Click 是 event,所以只要程序中使用 +=,则自动编译成 add_Click();使用 -=,则自动编译成 remove_Click()。
结论
函数指针、多态、反射、delegate,彼此之间互有关连,也各有优缺点。从函数指针演化到 delegate 的这段过程中,我对于这些机制设计者的巧思益发感到敬佩。
作者:蔡學鏞 更新日期:2004-11-07
来源:MDSN台湾
浏览次数:
相关文章
相关评论 发表评论
- No Comments