** Managed Extensions Bring .NET CLR Support to C++中文版(下篇) **
作者: Chris Sells
译者:荣耀
** 托管的类和接口 **
当你使用C++托管的扩展编译时,缺省来说,你将得到托管的代码(它使你可以访问托管的类型,而不是非托管的类型)。如果你希望你的类被托管,你需要使用新的managed C++关键字:__gc。一旦你这样做,并且假如你希望你的类可被外界使用,你可以使用关键字public。表3展示了一个在managed C++中实现你的.NET 类Talker的例子。可以这样编译该类:
cl /LD /CLR talker.cpp /o talker.dll
** 表 3 ** ** Managed C++ 类Talker **
// talker.cpp
#using
1<mscorlib.dll>
2
3using namespace System;
4
5namespace MsdnMagSamples
6
7{
8
9public __gc class Talker
10
11{
12
13public:
14
15String* Something;
16
17void SaySomething() {Console::WriteLine(Something);}
18
19~Talker() {Console::WriteLine(S"~Talker");}
20
21};
22
23}
24
25---
26
27这里有三个值得一提的有趣的东西。第一,我给托管的类加了个析构器,或者至少看起来是。还记得我说过NET类型并不真的有析构器而只有一个可选的Finalize方法吗?唔,因为C++程序员对那个记号是如此习惯,managed C++小组决定将managed C++析构器映射到Finalize的一个实现上,并加入一个对基类Finalize方法的调用。C#小组也是这么干的,但是记住,这两种语言中都不存在传统C++意义上的析构器。
28
29第二个有趣的东西是我将公开的数据成员直接暴露给.NET客户。在.NET中,数据成员称为字段,这意味着它们是没有代码的数据,并且,就象C++类中公开数据成员是个坏注意一样,字段(译注:指公开的)对于.NET类来说也是个坏主意,因为它们使你无法对值做一些计算、验证或将其设为只读。在C++中,你使用getter和setter函数来暴露数据成员。在.NET中,你可以使用属性来达到同样的效果。属性就是暴露数据的函数,但是,在某种程度上,允许你向组件加入一些代码。Managed C++利用关键字__property并以get_和set_作为前缀来指明一个属性,如下:
30
31public __gc class Talker
32
33{
34
35private:
36
37String* m_something;
38
39public:
40
41__property String* get_Something() {return m_something;}
42
43__property void set_Something(String* something) {m_something = something;}
44
45//...
46
47};
48
49如果你希望计算输出或验证输入,你可以分别在 get_或set_函数中做。同样,如果你想把属性设为只读或只写,只要移去相应的set_或get_函数即可。在客户端,字段和属性的写法是相同的:
50
51t->Something = "Greetings Planet Earth";
52
53然而,在字段和属性两种访问数据数据方式之间作切换时要小心。作为你的设计的正当理由,看起来好像很容易从字段开始然后变换为属性方式。不幸的是,字段访问和属性访问的潜在的 IL是不同的,因此,如果你将一个字段改变为属性,那么原先使用该字段的客户将会引发一个运行时异常。倘若你果真作了改变,你的客户必需重新编译。
54
55再看一眼表 3中的managed C++类Talker,可注意到它被直接暴露给所有的.NET客户。这个把戏COM玩不了。COM只能通过接口暴露功能。实现某COM接口的C++类的公开的方法未必可使用—除非这个方法是接口中的一部分。.NET无需将功能分别通过接口暴露。然而,在暴露泛化的功能时,接口依然重要。为了在managed C++中定义一个.NET接口,你可以和关键字__gc一起使用关键字__interface,如下:
56
57public __gc __interface ICanTalk
58
59{
60
61void Talk();
62
63};
64
65public __gc class Talker : public ICanTalk
66
67{
68
69//...
70
71// IcanTalk
72
73void Talk() {SaySomething();}
74
75};
76
77由于客户可以访问 Talker类,它就可以象调用其它公开的方法那样,调用IcanTalk方法。或者,如果客户有一个对基类的引用(所有托管的类型都最终派生于System::Object),它就可以转换为该类型。Managed C++客户可以通过dynamic_cast来转换,它已被升级以支持.NET类型转换,或使用一个称为__try_cast的转换符,如果转换失败,它将抛出一个异常:
78
79void MakeTalk(Object* obj)
80
81{
82
83try
84
85{
86
87ICanTalk* canTalk = __try_cast<icantalk*>(obj);
88
89canTalk->Talk();
90
91}
92
93catch(InvalidCastException*)
94
95{
96
97Console::WriteLine(S"Can't talk right now...");
98
99}
100
101}
102
103** 混用托管的和非托管的代码 **
104
105当你将项目中的文件设为/CLR选项时,你将得到托管的代码,它使你可以访问托管的类型。如果你希望将你的某个代码片断保持为非托管的,你可以使用一个新的#pragma语句:
106
107//mixed.cpp
108
109//...缺省为托管的代码...
110
111#pragma unmanaged
112
113//...非托管的代码...
114
115#pragma managed
116
117//...托管的代码...
118
119#pragma使你能够在同一模块里混用托管的和非托管的代码。尽管用不用由你,但在模块里使用非托管的代码和访问非托管的库或DLL没什么两样,你要注意一些约束(甚至使用Visual Basic的程序员也仍然调用DLL函数)。一旦你要从非托管的代码中调用托管的代码,如果你试图传递指向托管的类型的指针你务必要小心。
120
121例如,设想你希望调用 VarI4FromI2,将一个托管的堆上指向long的指针传给它,如下:
122
123HRESULT __stdcall VarI4FromI2(short sIn, long* plOut);
124
125__gc struct ShortLong
126
127{
128
129short n;
130
131long l;
132
133};
134
135void main()
136
137{
138
139ShortLong* sl = new ShortLong;
140
141sl->n = 10;
142
143VarI4FromI2(sl->n, &sl->l); //编译时错误
144
145}
146
147幸运的是,编译器会阻止这种行为,因为一旦你将一个托管的指针传入非托管的代码,垃圾收集器会丢掉对它的跟踪,当下一次运行时,垃圾收集器会轻易移走指针所指向的这个对象。
148
149为了避免发生该问题,你必须在作用域里显式地将该对象固定住,这样,垃圾收集器就知道不要动这个对象。可以使用关键词 __pin来达到这个目的:
150
151void main()
152
153{
154
155ShortLong* sl = new ShortLong;
156
157sl->n = 10;
158
159long __pin* pn = &sl->l;
160
161VarI4FromI2(sl->n, pn);
162
163}
164
165一旦这个被固定住的变量出了作用域,在其托管的内存上的锁将会被拿掉,垃圾收集器就可以随意将其移来移去(译注:垃圾收集器将对象在内存中移来移去,是为了减少内存碎片,有效利用内存,提高应用程序效率,故此处的 move未必是将对象移走、销毁。上文中关于GC的move一词,也多为此意)。
166
167** 值类型 **
168
169** ** 迄今为止,我们已经讨论了 .NET引用类型的定义和使用。引用类型配置于托管的堆上并被垃圾收集器销毁。另一方面,.NET值类型,是一种配置在栈上的类型(除非它作为一个引用类型的成员),并在栈释放的时候被销毁。值类型被用作非常简单的组合类型,它没有被垃圾收集器管理的负担。例如,在managed C++中,一个典型的值类型可使用关键字__value来声明:
170
171__value struct Point
172
173{
174
175Point(long _x, long _y) : x(_x), y(_y) {}
176
177long x;
178
179long y;
180
181};
182
183注意,我的 Point值类型有一个构造器。所有的值类型都具有的另一个构造器是缺省构造器,它将所有成员清零。例如,你可以用如下两种方式配置这个值类型:
184
185Point pt1; //(x, y) == (0, 0)
186
187Point pt2(1, 2); //(x, y) == (1, 2)
188
189尤其有趣的是,同样可象处理引用类型那样处理值类型。这是有用的,当你希望将一个值类型传递给一个带有引用类型参数的方法时,比如,把它加入集合。例如,为了使用 WriteLine输出我的Point的x和y的值,我可能想这么做:
190
191Console::WriteLine(S"({0}, {1})", pt.x, pt.y);
192
193不幸的是,这无法编译,因为 WriteLine需要一个格式化的字符串和一个类型为System.Object的对象引用列表。(WriteLine使用基类方法ToString来请求一个对象的可打印的字符串表示)。然而,你可以通过装箱而将值类型转换为引用类型。对一个值类型的装箱动作就是在托管的堆上配置相应的引用类型并将值拷贝入新的内存。为了在managed C++中装箱一个值,可使用操作符__box:
194
195Console::WriteLine(S"({0}, {1})", __box(pt.x), __box(pt.y));
196
197值类型是一个创建简单、高效类型的途径,而装箱则让你能够在需要的时候获得引用类型的多态好处。
198
199** 特性( Attributes) **
200
201如果说C++基于类,COM基于接口,那么.NET的核心应该是基于元数据。我所展示的不同的managed C++语言特性都在某种方式上依赖于元数据。然而,managed C++并没有暴露新的关键字或编译指示符来提供对所有这些元数据(可以设置在配件或类上)的访问。坦白地说,不可以这么做,这为变量和类型名称腾出很多的空间,特别是既然可获得的元数据特性完全是可扩展的。
202
203为了支持现在和将来的所有元数据类型,managed C++加入了一个全新的语法:特性语句块。特性语句块在要被特性化的类型前面指示以方括号。例如,.NET支持一些称为索引器(indexer)的东西,它其实是数组操作(在C++和C#中以方括号表示)的操作符重载的托管的等价物。然而,并没有__indexer关键字。相反,managed C++要求为该类被标记一个特性,以指明类的索引器:
204
205[System::Reflection::DefaultMemberAttribute(S"Item")]
206
207public __gc class MyCollection {__property String* get_Item(int index);};
208
209我们正讨论的特性 DefaultMemberAttribute实际上是一个定义于System::Reflection名字空间中的类,字符串“Item”是构造器参数,它指明属性Item作为类MyCollection的索引器。
210
211除了可为类设置特性外(还有类的成员),你也可以为配件设置特性。例如,如果你希望在一个配件上设置描述性的特性,你可以这么做:
212
213using namespace Reflection;
214
215[assembly:AssemblyTitle(S"My MSDN Magazine Samples")];
216
217实际上,编译器小组对特性是如此着迷,他们加入了一大把特性,以让你在等待 .NET的时候,可以编写非托管的代码。例如,如果你使用__interface关键字而未同时使用__gc关键字,你将得到一个COM接口,而不是.NET接口。新编译器还为其它成分提供了同样的便利,但你应该牢记,这些特性都不算是.NET。它们只是语言映射,以提供在C++和COM之间的平滑整合,并在后台生成IDL和ATL代码。若想了解更多关于C++非托管的扩展,可参见“ C++ Attributes: Make COM Programming a Breeze with New Feature in Visual Studio .NET ”。
218
219** 我们到哪儿啦? **
220
221不幸的是,尽管managed C++如此富有威力和弹性,但它并非.NET本地语言,这意味着书籍、文章、课程和代码例子等等将不大会用managed C++来编写,它们将会用C#来编写。但这没什么大惊小怪的,C++从来都没有成为任何流行平台上的本地语言。Unix和Win32使用C,Mac使用Pascal,NeXT使用Objective C(首先),COM使用Visual Basic(译注:C++程序员同意吗? J ),只有 BeOS把C++作为其本地语言,还记得你最后一次写BeOS代码的时间吗?.NET钟情于C#的事实仅仅意味着另一个语言将被翻译成C++等价物,就象自1983年以来的一样 J 。表 4展示了C#主要成分列表,同时还展示了它们是如何映射对应的managed C++语法的。
222
223** 表 4 ** ** Managed C++ Rosetta Stone **
224
225---
226
227** Managed操作 **
228
229|
230
231** C# **
232
233|
234
235** Managed C++ **
236
237声明一个接口
238
239|
240
241interface IFoo {}
242
243|
244
245__gc __interface IFoo {};
246
247声明一个类
248
249|
250
251class Foo {}
252
253|
254
255__gc class Foo {};
256
257声明一个属性
258
259|
260
261int x { get; set; }
262
263|
264
265__property int get_x();
266
267__property void set_x(int x);
268
269实现一个属性
270
271|
272
273int x {
274
275get { return m_x; }
276
277set { m_x = x; }
278
279}
280
281|
282
283__property int get_x() {return m_x;}
284
285__property void set_x(int x) {m_x = x;}
286
287实现一个接口
288
289|
290
291class Foo : IFoo {}
292
293|
294
295class Foo : public IFoo {};
296
297声明一个委托
298
299|
300
301delegate void CallMe();
302
303|
304
305__delegate void CallMe();
306
307声明一个索引器
308
309|
310
311String this[int index] {...}
312
313|
314
315[System::Reflection::DefaultMemberAttribute
316
317(S"Item")]
318
319__gc class MyCollection {
320
321__property String* get_Item(int index);
322
323};
324
325引用一个配件
326
327|
328
329/r:assembly.dll
330
331|
332
333#using <assembly.dll>
334
335引入名字空间
336
337|
338
339using System;
340
341|
342
343using namespace System;
344
345对象变量
346
347|
348
349IFoo foo = new Foo();
350
351|
352
353IFoo* pFoo = new Foo();
354
355成员访问
356
357|
358
359foo.DoFoo();
360
361|
362
363pFoo->DoFoo();
364
365引用一个名字空间
366
367|
368
369System.Console.WriteLine("");
370
371|
372
373System::Console::WriteLine("");
374
375<TD style="BORDER-RIGHT: windowtext 0.5pt solid; PADDING-RIGHT: 5.4pt; BORDER-TOP: #d4d0c8; PADDING-LEFT: 5.4pt; PADDING-BOTTOM: 0cm; BORDER-LEFT: windowtext 0.5pt solid; WIDTH: 86.4pt; PADDING-TOP: 0cm; BORDER-BOTTOM: windowtext 0.5pt solid; BACKGROUND-COLOR: transparent; mso-b</assembly.dll></icantalk*></mscorlib.dll>