分享
 
 
 

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

王朝c#·作者佚名  2008-06-01
窄屏简体版  字體: |||超大  

简介

类型安全机制的实现原来采用的是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 ...

}

}

更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或

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开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或

创建一个委托类

拥有类型安全成员函数指针是我们效仿.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基类。

更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或 维护委托集

在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返回的委托的。

更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或

托管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.

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

[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.

<http://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相当于一种解决方式。

更多内容请看.NET开发手册

.NET实用开发 .NET移动与嵌入式技术专题,或

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有