函数指针的进化论(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 版本可就难说了。

编译器帮我们产生的构造函数需要两个参数,第一个是方法所属的对象,第二个是方法在「Methodmetadata table 中的位置。编程员当然不会知道这个位置是几号 (但是编译器知道),所以编程员无法直接使用此构造函数。事实上,产生 delegate 对象的过程中充满离奇,有许多语法甜头,下面会一一解释。

你可以用下面的方式,来产生一个非静态方法的 delegate

new MyDelegate(myObject.MyNonStaticMethod);

编译器会自动调用 MyDelegate 构造函数,第一个参数是 myObject,第二个参数是 MyNonStaticMethod 方法在「Methodmetadata table中的位置。

你也可以用下面的方式,来产生一个静态方法的 delegate

new MyDelegate(MyClass.MyStaticMethod);

编译器会自动调用 MyDelegate 构造函数,第一个参数是 null,第二个参数是 MyStaticMethod 方法在 Methodmetadata 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 相同,以此例来说,方法参数必须是 intdouble,而传出值必须是 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 关键词 (包括作法二),就会使得编译器将它记录在「EventMetadata Table 内。有没有纪录这个对于执行时的毫无影响,但是可以帮助编译器等工具软件判读,来简化原代码。例如,使用作法一,无法用下面的方式来注册以及取消注册。

yourButton.Click += new YourDelegate(MyClass.MyStaticMethod);
yourButton.Click -= new YourDelegate(MyClass.MyStaticMethod);

但是,使用作法二和三,则可以用这种方式来注册以及取消注册。因为编译器从「EventMetadata Table 内发现 Click event,所以只要程序中使用 +=,则自动编译成 add_Click();使用 -=,则自动编译成 remove_Click()

结论

函数指针、多态、反射、delegate,彼此之间互有关连,也各有优缺点。从函数指针演化到 delegate 的这段过程中,我对于这些机制设计者的巧思益发感到敬佩。

 

 

作者:蔡學鏞   更新日期:2004-11-07
来源:MDSN台湾   浏览次数:

相关文章

相关评论   发表评论