Managed Extensions Bring .NET CLR Support to C++中文版(下篇)
作者:Chris Sells
译者:荣耀
托管的类和接口
当你使用C++托管的扩展编译时,缺省来说,你将得到托管的代码(它使你可以访问托管的类型,而不是非托管的类型)。如果你希望你的类被托管,你需要使用新的managedC++关键字:__gc。一旦你这样做,并且假如你希望你的类可被外界使用,你可以使用关键字public。表3展示了一个在managed C++中实现你的.NET类Talker的例子。可以这样编译该类:
cl /LD /CLR talker.cpp /o talker.dll
表3 Managed C++ 类Talker
// talker.cpp
#using <mscorlib.dll>
using namespace System;
namespace MsdnMagSamples
{
public __gc class Talker
{
public:
String* Something;
void SaySomething() {Console::WriteLine(Something);}
~Talker() {Console::WriteLine(S"~Talker");}
};
}
这里有三个值得一提的有趣的东西。第一,我给托管的类加了个析构器,或者至少看起来是。还记得我说过NET类型并不真的有析构器而只有一个可选的Finalize方法吗?唔,因为C++程序员对那个记号是如此习惯,managed C++小组决定将managed C++析构器映射到Finalize的一个实现上,并加入一个对基类Finalize方法的调用。C#小组也是这么干的,但是记住,这两种语言中都不存在传统C++意义上的析构器。
第二个有趣的东西是我将公开的数据成员直接暴露给.NET客户。在.NET中,数据成员称为字段,这意味着它们是没有代码的数据,并且,就象C++类中公开数据成员是个坏注意一样,字段(译注:指公开的)对于.NET类来说也是个坏主意,因为它们使你无法对值做一些计算、验证或将其设为只读。在C++中,你使用getter和setter函数来暴露数据成员。在.NET中,你可以使用属性来达到同样的效果。属性就是暴露数据的函数,但是,在某种程度上,允许你向组件加入一些代码。Managed C++利用关键字__property并以get_和set_作为前缀来指明一个属性,如下:
public __gc class Talker
{
private:
String* m_something;
public:
__property String* get_Something() {return m_something;}
__property void set_Something(String* something) {m_something = something;}
//...
};
如果你希望计算输出或验证输入,你可以分别在get_或set_函数中做。同样,如果你想把属性设为只读或只写,只要移去相应的set_或get_函数即可。在客户端,字段和属性的写法是相同的:
t->Something = "Greetings Planet Earth";
然而,在字段和属性两种访问数据数据方式之间作切换时要小心。作为你的设计的正当理由,看起来好像很容易从字段开始然后变换为属性方式。不幸的是,字段访问和属性访问的潜在的IL是不同的,因此,如果你将一个字段改变为属性,那么原先使用该字段的客户将会引发一个运行时异常。倘若你果真作了改变,你的客户必需重新编译。
再看一眼表3中的managed C++类Talker,可注意到它被直接暴露给所有的.NET客户。这个把戏COM玩不了。COM只能通过接口暴露功能。实现某COM接口的C++类的公开的方法未必可使用—除非这个方法是接口中的一部分。.NET无需将功能分别通过接口暴露。然而,在暴露泛化的功能时,接口依然重要。为了在managed C++中定义一个.NET接口,你可以和关键字__gc一起使用关键字__interface,如下:
public __gc __interface ICanTalk
{
void Talk();
};
public __gc class Talker : public ICanTalk
{
//...
// IcanTalk
void Talk() {SaySomething();}
};
由于客户可以访问Talker类,它就可以象调用其它公开的方法那样,调用IcanTalk方法。或者,如果客户有一个对基类的引用(所有托管的类型都最终派生于System::Object),它就可以转换为该类型。Managed C++客户可以通过dynamic_cast来转换,它已被升级以支持.NET类型转换,或使用一个称为__try_cast的转换符,如果转换失败,它将抛出一个异常:
void MakeTalk(Object* obj)
{
try
{
ICanTalk* canTalk = __try_cast<ICanTalk*>(obj);
canTalk->Talk();
}
catch(InvalidCastException*)
{
Console::WriteLine(S"Can't talk right now...");
}
}
混用托管的和非托管的代码
当你将项目中的文件设为/CLR选项时,你将得到托管的代码,它使你可以访问托管的类型。如果你希望将你的某个代码片断保持为非托管的,你可以使用一个新的#pragma语句:
//mixed.cpp
//...缺省为托管的代码...
#pragma unmanaged
//...非托管的代码...
#pragma managed
//...托管的代码...
#pragma使你能够在同一模块里混用托管的和非托管的代码。尽管用不用由你,但在模块里使用非托管的代码和访问非托管的库或DLL没什么两样,你要注意一些约束(甚至使用Visual Basic的程序员也仍然调用DLL函数)。一旦你要从非托管的代码中调用托管的代码,如果你试图传递指向托管的类型的指针你务必要小心。
例如,设想你希望调用VarI4FromI2,将一个托管的堆上指向long的指针传给它,如下:
HRESULT __stdcall VarI4FromI2(short sIn, long* plOut);
__gc struct ShortLong
{
short n;
long l;
};
void main()
{
ShortLong* sl = new ShortLong;
sl->n = 10;
VarI4FromI2(sl->n, &sl->l); //编译时错误
}
幸运的是,编译器会阻止这种行为,因为一旦你将一个托管的指针传入非托管的代码,垃圾收集器会丢掉对它的跟踪,当下一次运行时,垃圾收集器会轻易移走指针所指向的这个对象。
为了避免发生该问题,你必须在作用域里显式地将该对象固定住,这样,垃圾收集器就知道不要动这个对象。可以使用关键词__pin来达到这个目的:
void main()
{
ShortLong* sl = new ShortLong;
sl->n = 10;
long __pin* pn = &sl->l;
VarI4FromI2(sl->n, pn);
}
一旦这个被固定住的变量出了作用域,在其托管的内存上的锁将会被拿掉,垃圾收集器就可以随意将其移来移去(译注:垃圾收集器将对象在内存中移来移去,是为了减少内存碎片,有效利用内存,提高应用程序效率,故此处的move未必是将对象移走、销毁。上文中关于GC的move一词,也多为此意)。
值类型
迄今为止,我们已经讨论了.NET引用类型的定义和使用。引用类型配置于托管的堆上并被垃圾收集器销毁。另一方面,.NET值类型,是一种配置在栈上的类型(除非它作为一个引用类型的成员),并在栈释放的时候被销毁。值类型被用作非常简单的组合类型,它没有被垃圾收集器管理的负担。例如,在managed C++中,一个典型的值类型可使用关键字__value来声明:
__value struct Point
{
Point(long _x, long _y) : x(_x), y(_y) {}
long x;
long y;
};
注意,我的Point值类型有一个构造器。所有的值类型都具有的另一个构造器是缺省构造器,它将所有成员清零。例如,你可以用如下两种方式配置这个值类型:
Point pt1; //(x, y) == (0, 0)
Point pt2(1, 2); //(x, y) == (1, 2)
尤其有趣的是,同样可象处理引用类型那样处理值类型。这是有用的,当你希望将一个值类型传递给一个带有引用类型参数的方法时,比如,把它加入集合。例如,为了使用WriteLine输出我的Point的x和y的值,我可能想这么做:
Console::WriteLine(S"({0}, {1})", pt.x, pt.y);
不幸的是,这无法编译,因为WriteLine需要一个格式化的字符串和一个类型为System.Object的对象引用列表。(WriteLine使用基类方法ToString来请求一个对象的可打印的字符串表示)。然而,你可以通过装箱而将值类型转换为引用类型。对一个值类型的装箱动作就是在托管的堆上配置相应的引用类型并将值拷贝入新的内存。为了在managed C++中装箱一个值,可使用操作符__box:
Console::WriteLine(S"({0}, {1})", __box(pt.x), __box(pt.y));
值类型是一个创建简单、高效类型的途径,而装箱则让你能够在需要的时候获得引用类型的多态好处。
特性(Attributes)
如果说C++基于类,COM基于接口,那么.NET的核心应该是基于元数据。我所展示的不同的managed C++语言特性都在某种方式上依赖于元数据。然而,managed C++并没有暴露新的关键字或编译指示符来提供对所有这些元数据(可以设置在配件或类上)的访问。坦白地说,不可以这么做,这为变量和类型名称腾出很多的空间,特别是既然可获得的元数据特性完全是可扩展的。
为了支持现在和将来的所有元数据类型,managed C++加入了一个全新的语法:特性语句块。特性语句块在要被特性化的类型前面指示以方括号。例如,.NET支持一些称为索引器(indexer)的东西,它其实是数组操作(在C++和C#中以方括号表示)的操作符重载的托管的等价物。然而,并没有__indexer关键字。相反,managed C++要求为该类被标记一个特性,以指明类的索引器:
[System::Reflection::DefaultMemberAttribute(S"Item")]
public __gc class MyCollection {__property String* get_Item(int index);};
我们正讨论的特性DefaultMemberAttribute实际上是一个定义于System::Reflection名字空间中的类,字符串“Item”是构造器参数,它指明属性Item作为类MyCollection的索引器。
除了可为类设置特性外(还有类的成员),你也可以为配件设置特性。例如,如果你希望在一个配件上设置描述性的特性,你可以这么做:
using namespace Reflection;
[assembly:AssemblyTitle(S"My MSDN Magazine Samples")];
实际上,编译器小组对特性是如此着迷,他们加入了一大把特性,以让你在等待.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”。
我们到哪儿啦?
不幸的是,尽管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++语法的。
表4 Managed C++ Rosetta Stone
Managed操作
C#
Managed C++
声明一个接口
interface IFoo {}
__gc __interface IFoo {};
声明一个类
class Foo {}
__gc class Foo {};
声明一个属性
int x { get; set; }
__property int get_x();
__property void set_x(int x);
实现一个属性
int x {
get { return m_x; }
set { m_x = x; }
}
__property int get_x() {return m_x;}
__property void set_x(int x) {m_x = x;}
实现一个接口
class Foo : IFoo {}
class Foo : public IFoo {};
声明一个委托
delegate void CallMe();
__delegate void CallMe();
声明一个索引器
String this[int index] {...}
[System::Reflection::DefaultMemberAttribute
(S"Item")]
__gc class MyCollection {
__property String* get_Item(int index);
};
引用一个配件
/r:assembly.dll
#using <assembly.dll>
引入名字空间
using System;
using namespace System;
对象变量
IFoo foo = new Foo();
IFoo* pFoo = new Foo();
成员访问
foo.DoFoo();
pFoo->DoFoo();
引用一个名字空间
System.Console.WriteLine("");
System::Console::WriteLine("");
声明一个枚举
enum Foo { bar, quux }
__value enum Foo { bar, quux };
冲突时的解决方案
void IArtist.Draw() {...}
void ICowboy.Draw() {...}
void IArtist::Draw() {...}
void ICowboy::Draw() {...}
值类型
struct Foo {...}
__value struct Foo {...};
抽象类型
abstract class Foo {...}
__abstract class Foo {...};
封闭的类型
sealed class Foo {...}
__sealed class Foo {...};
C风格的转换
try { IFoo foo = (IFoo)bar; }
catch( IllegialCastException
e) {...}
try { IFoo* pFoo = __try_cast<IFoo*>(pBar); }
catch( IllegialCastException* e )
{...}
动态转换
(dynamic cast)
IFoo foo = bar as IFoo;
If( foo != null ) ...
IFoo* pFoo = dynamic_cast<IFoo*>(pBar);
if(pFoo !=0)...
类型检查
If( bar is IFoo ) ...
if( dynamic_cast<IFoo*>(pBar) != 0 ) ...
异常处理
try {...}
catch(MyException e) {...}
finally {...}
try {...}
catch(MyException* pe) {...}
__finally {...}
这些新特性把你现存的代码都摆到了什么位置?managed C++的开发为你特别提供了一个温和的方式,以将你现有的代码移植到.NET中。如果你喜欢使用托管的类型,你要做的只是轻拨/CLR开关并重新编译。你现存在的代码将按照你希望的那样继续工作,包括你的ATL和MFC项目。
也许你喜欢使用.NET框架中托管的类型,也许你喜欢使用一些你自己的或你小组的用C#或Visual Basic创建的东西,不管哪一种,使用#using即可。也许你希望暴露已存在的C++类型托管的包装,就象多年来你对COM做的包装一样,如果是这样,public __gc将使你心想事成。
实际上,微软已经做了令人惊奇的工作,使你可以在managed C++中混用托管的和非托管的类型和代码,并让你决定哪些代码、在什么时候,移植到.NET。
-全文完-