使用C++深入研究.NET的委托与事件(转)

撰文 J. Daniel Smith 翻译 曾毅 最后更新:2004年3月14日 声明:译文发表于《Dr.Dobb's Journal软件研发》杂志,版权归《Dr.Dobb's Journal软件研发》杂志所有 想要真正掌握委托与事件最好的方法便是你自己来实现它们—使用以前的纯C++。 简介 类型安全机制的实现原来采用的是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 (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 (&Str::Len); (void) printf("%s is %ld chars/n", s, (str.*f)(s)); } void crashAndBurn(const char* s) { Str str; FUNC f = static_cast (&Str::Cat); (str.*f)(s); } static_cast运算符将转化Str::Len函数指针,因为Str是由Object派生来的,但是Str::Cat是类型安全的,它不能被转换,因为函数签名是不匹配的。 成员函数指针的工作机制与常规的函数指针是非常相似的;唯一不同(除了更为复杂的语法外)的是你需要一个用来调用成员函数的类的实例。当然,我们也可以使用->*运算符来用指向类实例的指针完成对成员函数的调用。 Str* pStr = new Str(); FUNC f = static_cast (&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 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 struct void1_T { typedef void (Object::*mf_t)(ARG1); }; template void Invoke(void1_T ::mf_t mf, ARG1 v1, ARG2) const { (this->*mf)(v1); } }; template class ObjectT : public Object {}; typedef ObjectT VoidType; 这个Object基类包含了一个typedef对应每一个成员函数签名;我使用了void返回类型来简化了很多需要做的工作。Typedef可以参照如下方式使用: typedef Object::void1_T ::mf_t StringMF_t; 我们使用了std::string类型的参数和void返回类型就能够非常容易地为成员函数指针创建typedef。 程序根据附加的参数对于Invoke是跟踪计数的。这是非常必要的,因为对于所有的Invoke方法必须有同样数目的参数;重载决策基于第一个参数—成员函数指针的类型,来完成。需要注意的是大部分的.NET Framework将在委托中使用EventArgs对象来避免上述的复杂情况。你可以通过从EventArgs派生来添加额外的参数而不需要给委托添加签名。 最后,ObjectT模版提供了一个简单的方法用来产生唯一类型,每一个类型最终是从Object派生来的。这就确保了类型安全。 基于上面所有的内容,委托类现在就应当是如下所示的样子: template class DelegateT_ : public ObjectT { 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 class MulticastDelegateT : public DelegateT_ { typedef DelegateT_ Delegate; typedef std::list 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 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 struct DelegateT : public MulticastDelegateT { DelegateT(Object& o, const MF_T& mf) : MulticastDelegateT (o, mf) {} DelegateT() {} typedef DelegateT Event; }; 它的主要功能是提供事件typedef。 将他们集成起来 现在你可以用C++编写实现C#例子当中的DelegatesAndEvents类了: class DelegatesAndEvents { // C#: public delegate void PrintString(string s); typedef DelegateT ::mf_t, std::string> PrintString_; public: template static PrintString_ PrintString(OBJECT& o, void (OBJECT::*mf)(std::string)) { return PrintString_(o, static_cast ::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 { // ... 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. < http://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. . [3] Jeffrey Richter. “Delegates, Part 2,” MSDN Magazine, June 2001. < http://msdn.microsoft.com/msdnmag/issues/01/06/net/default.aspx> [4] Bobby Schmidt. “The Red Pill,” April 23, 2002. 作者简介 J. Daniel Smith 是密歇根州Novi的一位持有Autodesk认证的软件工程师。他从加尔文学院取得了理学学士,并在密歇根州立大学取得了计算机科学的理学硕士学位。你可以通过 cuj@jdanielsmith.org 与他取得联系。 译者注: 译注1:Type-safe:按照2003年微软官方提供的术语表翻译为“类型安全”。 译注2:overload resolution: 按照2003年微软官方提供的术语表翻译为“重载决策”。 译注3:原文中所列参考文献的地址已经失效,译文中提供的是在本文翻译截稿时所示参考的最新有效链接,为尊重原著者特此说明。 译注4:destructor一词按照简体中文常用译法译为“反引用”。 译注5:关于文中采用的reinterpret_cast。事实上,reinterpret_cast在这里是通不过的。因为我们不可能对成员函数指针进行所谓的类型转换。这个例子实际上是在比较对象,转换的也是对象,而不是对象的成员。而这个示例却将reinterpret_cast作为解决的方式,即直接比较的是对象的成员,而不考虑对象。也就是说,试图转换对象的成员。而失去类型转换的真正意图。为什么作者在这里用了 reinterpret_cast,意为“重新意义上的强制转换“。这种转换并不是基于类型或者是对象的,更谈不上类型安全了。委托的本质上讲是函数指针,不过,它需要首先进行类型检查。我们说委托对象的存在,只是为了类型检查,真正有意义的还是其方法。所以reinterpret_cast相当于一种解决方式。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值