Thursday, August 19, 2004

 

Some notes on type safe in DotNet

1.
原文:http://www.cuj.com/documents/s=8009/cuj0209smith/
翻译:http://tech.tfol.com/techs/wlxx/block/html/2004090300525.html

使用C++深入研究.NET委托与事件

翻译 曾毅

  简介

  类型安全机制的实现原来采用的是C风格的回调(callback)函数,而.NET Framework引入了委托和事件来替代原来的方式;它们被广泛地使用。我们在这里尝试使用标准C++来实现与之类似的功能,这样我们不但可以对这些概念有一个更好的认识,而且同时还能够体验C++的一些有趣的技术。

  C#中的委托与事件关键字

  首先我们来看一个简单的C#程序(下面的代码略有删节)。执行程序的输出结果如下显示:

  SimpleDelegateFunction called from Ob1,

  string=Event fired!

  Event fired!(Ob1): 3:49:46 PM on

  Friday, May 10, 2002

  Event fired!(Ob1): 1056318417

  SimpleDelegateFunction called from Ob2,

  string=Event fired!

  Event fired!(Ob2): 3:49:46 PM on

  Friday, May 10, 2002

  Event fired!(Ob2): 1056318417

  所有这些都源于这样一行代码:dae.FirePrintString("Event fired!");

  在利用C++来实现这些功能时,我模仿了C#的语法并完全按照功能的要求进行开发。

  namespace DelegatesAndEvents

  {

  class DelegatesAndEvents

  {

  public delegate void PrintString(string s);

  public event PrintString MyPrintString;

  public void FirePrintString(string s)

  {

  if (MyPrintString != null)MyPrintString(s);

  }

  }

  class TestDelegatesAndEvents

  {

  [STAThread]

  static void Main(string[] args)

  {

  DelegatesAndEvents dae =new DelegatesAndEvents();

  MyDelegates d = new MyDelegates();

  d.Name = "Ob1";

  dae.MyPrintString +=new DelegatesAndEvents.PrintString(d.SimpleDelegateFunction);

  // ... more code similar to the

  // above few lines ...

  dae.FirePrintString("Event fired!");

  }

  }

  class MyDelegates

  {

  // ... "Name" property omitted...

  public void SimpleDelegateFunction(string s)

  {

  Console.WriteLine("SimpleDelegateFunction called from {0}, string={1}", m_name, s);

  }

  // ... more methods ...

  }

  }

C++中的类型安全函数指针

  对于“老式方法”的批判之一便是它们不是类型安全的[1]。下面的代码证明了这个观点:

  typedef size_t (*FUNC)(const char*);

  void printSize(const char* str) {

  FUNC f = strlen;

  (void) printf("%s is %ld chars\n", str, f(str));

  }

  void crashAndBurn(const char* str) {

  FUNC f = reinterpret_cast<FUNC>(strcat);

  f(str);

  }

  代码在[2]中可以找到。当然,在你使用reinterpret_cast的时候,你可能会遇到麻烦。如果你将强制转换(cast)去掉,C++编译器将报错,而相对来说更为安全的static_cast也不能够完成转换。这个例子也有点像比较苹果和橙子,因为在C#中万事万物皆对象,而reinterpret_cast就相当于一种解决方式。下面的这个C++程序示例将会采取使用成员函数指针的方法来避免使用reinterpret_cast:

  struct Object { };

  struct Str : public Object {

  size_t Len(const char* str) {

  return strlen(str);

  }

  char* Cat(char* s1, const char* s2) {

  return strcat(s1, s2);

  }

  };

  typedef size_t (Object::*FUNC)(const char*);

  void printSize(const char* s) {

  Str str;

  FUNC f = static_cast<FUNC>(&Str::Len);

  (void) printf("%s is %ld chars\n", s, (str.*f)(s));

  }

  void crashAndBurn(const char* s) {

  Str str;

  FUNC f = static_cast<FUNC>(&Str::Cat);

  (str.*f)(s);

  }

  static_cast运算符将转化Str::Len函数指针,因为Str是由Object派生来的,但是Str::Cat是类型安全的,它不能被转换,因为函数签名是不匹配的。

  成员函数指针的工作机制与常规的函数指针是非常相似的;唯一不同(除了更为复杂的语法外)的是你需要一个用来调用成员函数的类的实例。当然,我们也可以使用->*运算符来用指向类实例的指针完成对成员函数的调用。

  Str* pStr = new Str();

  FUNC f = static_cast<FUNC>(&Str::Len);

  (void) printf("%s is %ld chars\n", s, (str->*f)(s));

  delete pStr;

  只要所有的类是从基类Object派生来的(C#中就是这样),你就可以使用C++来创建类型安全的成员函数指针。

创建一个委托类

  拥有类型安全成员函数指针是我们效仿.NET功能的第一部。尽管如此,单独的成员函数指针是毫无用处的 — 你总是需要一个类的实例;委托对象同时保持在两边,使得调用成员函数非常方便。我们接着上面的例子续写下面的代码:

  struct StrLen_Delegate

  {

  typedef size_t (Str::*MF_T)(const char*);

  MF_T m_method;

  Object& m_pTarget;

  StrLen_Delegate(Object& o, const MF_T& mf) :

  m_pTarget(&o), m_method(mf) {}

  MF_T Method() const {

  return m_method;

  }

  Object& Target() const {

  return *m_pTarget;

  }

  size_t Invoke(const char* s) {

  (m_pTarget.*m_method)(s);

  }

  };

  void printSize2(const char* s) {

  Str str;

  StrLen_Delegate d(str, &Str::Len);

  (void) printf("%s is %ld chars\n", s,

  d.Invoke(s));

  }

  有了委托类,调用成员函数变得更为简单。使用运算符代替Invoke来给这个类创建一个仿函数将使调用降为仅有d(s);为了清晰以及和.NET规定匹配,我使用Invoke。需要注意的是,类的实例是一个对象(Object)而不是Str。只要签名匹配,从Object派生来的任何一个类的成员函数指针将允许被用于创建委托。

  这个类在这个例子中使用能够工作得非常好,但是它不是非常灵活;我们必须为每一个可能的成员函数签名写一个新的委托类。.NET使用由公用语言运行时(Common Language Runtime)维护的rich type信息来解决这个问题。但这在C++中不是一个非常可行的办法,但是可以采用模板来完成类似的功能。我们不用将Invoke函数的参数设为const char* s,而是将类型指定为模板参数:

  template <typename ARG1>

  struct StrLen_Delegate

  {

  typedef size_t (Str::*MF_T)(ARG1);

  // ... as above ...

  size_t Invoke(ARG1 v1) {

  (m_pTarget.*m_method)(v1);

  }

  };

  这样效果就好很多了,但是Invoke函数将只作用于单参数的成员函数。并且,委托也仅仅关心类的实例以及成员函数指针;它不是真正关心成员函数指针的细节。最后,我们很方便地就能够为成员函数指针产生一个typedef作为模版参数使用。由于一切都是由Object类派生出来的,这些细节也可以被移动到Object当中:

  struct Object

  {

  template <typename ARG1>

  struct void1_T {

  typedef void (Object::*mf_t)(ARG1);

  };

  template <typename ARG1, typename ARG2>

  void Invoke(void1_T<ARG1>::mf_t mf, ARG1 v1, ARG2) const {

  (this->*mf)(v1);

  }

  };

  template <typename CLASS>

  class ObjectT : public Object {};

  typedef ObjectT<void> VoidType;

  这个Object基类包含了一个typedef对应每一个成员函数签名;我使用了void返回类型来简化了很多需要做的工作。Typedef可以参照如下方式使用:

  typedef Object::void1_T<std::string>::mf_t StringMF_t;

  我们使用了std::string类型的参数和void返回类型就能够非常容易地为成员函数指针创建typedef。

  程序根据附加的参数对于Invoke是跟踪计数的。这是非常必要的,因为对于所有的Invoke方法必须有同样数目的参数;重载决策基于第一个参数—成员函数指针的类型,来完成。需要注意的是大部分的.NET Framework将在委托中使用EventArgs对象来避免上述的复杂情况。你可以通过从EventArgs派生来添加额外的参数而不需要给委托添加签名。

  最后,ObjectT模版提供了一个简单的方法用来产生唯一类型,每一个类型最终是从Object派生来的。这就确保了类型安全。

  基于上面所有的内容,委托类现在就应当是如下所示的样子:

  template <typename MF_T>

  class DelegateT_ : public ObjectT<MF_T>

  {

  MF_T m_method;

  Object* m_pTarget;

  protected:

  DelegateT_() : m_pTarget(NULL), m_method(NULL) {}

  DelegateT_(Object& o, const MF_T& mf) :

  m_pTarget(&o), m_method(mf) {}

  public:

  MF_T Method() const {

  return m_method;

  }

  Object& Target() const {

  return *m_pTarget;

  }

  };

  模板参数现在就是一个typedef成员函数指针(生成方法如上所示),而Invoke方法继承于Object基类。

维护委托集

  在C#中,Delegate和Event关键字成对出现用来创建一列委托,就像上面的第一个例子:

  new DelegatesAndEvents.PrintString(d.SimpleDelegateFunction);

  创建一个新的类似于我的C++实现的委托对象:

  StrLen_Delegate d(str, &Str::Len);

  MyPrintString对象是一个拥有重载运算符+=的事件,这是用来添加委托的。在C++中我们也可以模仿这个功能来完成类似的工作。C#中的Delegate关键字创建了一个MultiCastDelegate对象(详见[3])。你会注意到我将上面的委托类命名为DelegateT_(尾随的下划线说明这个名字是保留的)。严格地说,名字_DelegateT是为这个程序实现而保留的(__DelegateT也是一样的)因为下划线后跟随着一个大写字母。_delegateT也可以(仅有一个被小写字母尾随其后的下划线),但是我偏向于避免所有的由于前下划线所可能导致的潜在错误(阅读我写的代码的人很可能抓不到我的所有规则)也不愿意采用后划线代替它。保留DelegateT_是因为完成效仿.NET功能的委托类是从多播委托(MultiCastDelegate)类派生来的。

  Delegate对象可以很容易地被存储在标准C++容器中。我将使用list,因为它与.NET的工作机制是最接近的。依据你个人的需要,也可以使用vector或者deque。使用集(set)来提供不论委托被附加入几次,仅仅调用一次的有趣的特性。MultiCastDelegate的第一部分如下所示:

  template <typename MF_T, typename ARG1 = VoidType,

  typename ARG2 = VoidType>

  class MulticastDelegateT : public DelegateT_<MF_T>

  {

  typedef DelegateT_<MF_T> Delegate;

  typedef std::list<Delegate> Delegates_t;

  protected:

  MulticastDelegateT() {}

  public:

  MulticastDelegateT(Object& o, const MF_T& mf) :

  Delegate(o, mf) {}

  MulticastDelegateT& operator+=(const Delegate& d) {

  m_delegates.push_back(d);

  return *this;

  }

  private:

  Delegates_t m_delegates;

  };

  这里使用了list和几个typedef来存储委托集。它需要从DelegateT_派生而来,因为下面我将从MultiCastDelegateT派生出DelegateT作为真正的委托类。

  而后激发所有被存储的委托上的一个C#循环中的事件并调用每一个。因为我使用的是标准容器,使迭代器将很方便:

  void operator()(ARG1 v1 = VoidType(),

  ARG2 v2 = VoidType()) const {

  for (Delegates_t::const_iterator it = m_delegates.begin();

  it != m_delegates.end(); ++it)

  (it->Target()).Invoke(it->Method(), v1, v2);

  }

  即使你很适应标准C++容器,这可能也是你不熟悉的一行代码:只在一个模版类中就可以使用迭代器调用成员函数!对迭代器取反引用,我们可以清楚地看到发生了什么:

  const Delegate& d = *it;

  d.Invoke(d.Method(), v1, v2);

  如果你对迭代器还不是很适应,你可以指出一个就像数组一样的deque:

  for (int i=0; i<m_delegates.size(); i++)

  Delegate d = m_delegates[i];

  在这里,你可以为DelegateT_ 类添加下面的模板成员函数:

  template <typename ARG1, typename ARG2>

  void Invoke_(ARG1 v1 = ARG1(), ARG2 v2 = ARG2()) const {

  this->Invoke(m_method, v1, v2);

  }

  这样就避免了MultiCastDelegateT::Invoke方法一定要将成员函数指针传递给Object::Invoke:

  d.Invoke_(v1, v2);

尽管如此,这将需要每一个参数都有一个默认构造函数,但事实却不见得如此。并且,由于MultiCastDelegateT是真正的委托基类,看上去并没有太大的必要调用Object::Invoke 路径—即使由于这个原因代码显得更为复杂。(这也会在Visual C++.NET中导致可怕的“内部编译器错误”)。

  实际的委托类现在仅仅是MultiCastDelegateT的一个简单的包装:

  template <typename MF_T, typename ARG1 = VoidType,

  typename ARG2 = VoidType>

  struct DelegateT :

  public MulticastDelegateT<MF_T, ARG1, ARG2>

  {

  DelegateT(Object& o, const MF_T& mf) :

  MulticastDelegateT<MF_T, ARG1, ARG2>(o, mf) {}

  DelegateT() {}

  typedef DelegateT<MF_T, ARG1, ARG2> Event;

  };

  它的主要功能是提供事件typedef。

  将他们集成起来

  现在你可以用C++编写实现C#例子当中的DelegatesAndEvents类了:

  class DelegatesAndEvents

  {

  // C#: public delegate void PrintString(string s);

  typedef DelegateT<Object::void1_T<std::string>::mf_t,

  std::string> PrintString_;

  public:

  template <typename OBJECT>

  static PrintString_ PrintString(OBJECT& o,

  void (OBJECT::*mf)(std::string)) {

  return PrintString_(o,

  static_cast<Object::void1_T<std::string>::mf_t>(mf));

  }

  // C#: public event PrintString MyPrintString;

  PrintString_::Event MyPrintString;

  void FirePrintString(std::string s) {

  MyPrintString(s);

  }

  };

  这样的语法看上去着实令人恐怖,如果你愿意,可以用一些灵巧的宏来简化它。但最近宏的名声不太好,并且我们进行的这个主题关键是要了解细节。无论怎样,你都应当感谢C#编译器为你做的工作。

  第一行代码创建一个成员函数指针私有的typedef,名称为PrintString_。参数类型std::string需要列两次,这太糟了,但是这正是由于Visual C++不支持局部模版特化造成的。static方法为创建你自己的类型的委托提供了一个方便的方法,允许你这样来写你的代码:

  DelegatesAndEvents::PrintString_

  myDelegate = DelegatesAndEvents::PrintString(d,&MyDelegates::SimpleDelegateFunction);

  这与上面的C#代码是类似的。

  而后,我们使用来自DelegateT_的Event typedef创建事件。请注意这一系列的typedef是如何允许C++代码至少是有C#代码一些类似之处的。最后,有一个方法触发事件,这与C#尤其相同。(由于你采用的是标准容器,所以不必担心NULL列表。)

  使用委托和事件的客户端的代码就很明了了,而且也很类似于C#代码(同样这些代码也是略有缩减的):

  struct MyDelegates : public ObjectT<MyDelegates>

  {

  // ... Name omitted...

  void SimpleDelegateFunction(std::string s)

  {

  printf("SimpleDelegateFunction called from %s,

  string=%s\n", m_name.c_str(), s.c_str());

  }

  // ... more methods ...

  };

  void CppStyle()

  {

  DelegatesAndEvents dae;

  MyDelegates d;

  d.Name() = "Obj1";

  dae.MyPrintString += DelegatesAndEvents::PrintString

  (d, &MyDelegates::SimpleDelegateFunction);

  // ... more code similar to the above few lines ...

  dae.FirePrintString("Event fired!");

  }

  请注意MultiCastDelegateT::operator+=是如何被调用来为委托列表添加每一个由静态方法DelegatesAndEvents::PrintString返回的委托的。

托管C++

  由于委托和事件是.NET框架的一部分,所有的.NET支持的语言都可以使用它们。我所描述的基于模版的实现是专门针对C++的。Microsoft采用了不同的方法在C++中将这个功能公开—对于标准C++的扩展称为托管C++。也许你并不感到太吃惊,在托管C++中编写这个例子与最初的代码是那么相似:

  public __gc struct DelegatesAndEvents {

  __event void MyPrintString(String* s);

  void FirePrintString(String* s) {

  MyPrintString(s);

  }

  };

  __gc struct MyDelegates

  {

  String* Name;

  void SimpleDelegateFunction(String* s) {

  Console::WriteLine

  ("SimpleDelegateFunction called from {0} string={1}",Name, s);

  }

  };

  void ManagedCpp()

  {

  DelegatesAndEvents* dae = new DelegatesAndEvents();

  MyDelegates* d = new MyDelegates();

  d->Name = "Obj1";

  __hook(&DelegatesAndEvents::MyPrintString, dae,

  &MyDelegates::SimpleDelegateFunction, d);

  dae->FirePrintString(S"Event fired!");

  }

  关键字__gc标志着这个类是被垃圾回收机制控制的(托管的);我们不需要调用delete函数。仅仅一个__event关键字就完成了我们上面代码的大部分功能。需要注意的是托管C++使用__hook关键字来替代上面讨论的操作符+=。你会发觉使用-Fx标记[4]调用(托管)C++编译器编译上述代码和检查产生的结果文件.mrg非常有趣。在编译器级加入新功能而不是编写模板显然要容易得多了。

  结论

  通过使用极为高级的C++技巧,我已经向大家展示了用C++为简单的样例代码实现委托与事件是可行的。这个实现主要考虑基于.NET框架。更为一流和纯粹的C++解决方案可以使用C++标准库中的适配器和联编程序。

  参考文献

  [1] Jeffrey Richter. “An Introduction to Delegates,” MSDN Magazine, April 2001.

  < msdn.microsoft.com/msdnmag/issues/01/04/net/default.aspx >.

  [2] Richard Grimes. “.NET Delegates: Making Asynchronous Method Calls in the .NET Environment,” MSDN Magazine, August 2001.

  <msdn.microsoft.com/msdnmag/issues/01/08/Async/default.aspx>.

  [3] Jeffrey Richter. “Delegates, Part 2,” MSDN Magazine, June 2001.

  < msdn.microsoft.com/msdnmag/issues/01/06/net/default.aspx>

  [4] Bobby Schmidt. “The Red Pill,” April 23, 2002.

  <msdn.microsoft.com/library/default.asp?url=/library/en-us/dndeepc/html/deep04232002.asp >

  译者注:

  译注1:Type-safe:按照2003年微软官方提供的术语表翻译为“类型安全”。

  译注2:overload resolution: 按照2003年微软官方提供的术语表翻译为“重载决策”。

  译注3:原文中所列参考文献的地址已经失效,译文中提供的是在本文翻译截稿时所示参考的最新有效链接,为尊重原著者特此说明。

  译注4:destructor一词按照简体中文常用译法译为“反引用”。

  译注5:关于文中采用的reinterpret_cast。事实上,reinterpret_cast在这里是通不过的。因为我们不可能对成员函数指针进行所谓的类型转换。这个例子实际上是在比较对象,转换的也是对象,而不是对象的成员。而这个示例却将reinterpret_cast作为解决的方式,即直接比较的是对象的成员,而不考虑对象。也就是说,试图转换对象的成员。而失去类型转换的真正意图。为什么作者在这里用了reinterpret_cast,意为“重新意义上的强制转换“。这种转换并不是基于类型或者是对象的,更谈不上类型安全了。委托的本质上讲是函数指针,不过,它需要首先进行类型检查。我们说委托对象的存在,只是为了类型检查,真正有意义的还是其方法。所以reinterpret_cast相当于一种解决方式。



<< Home

This page is powered by Blogger. Isn't yours?