Managed Extensions Bring .NET CLR Support to C++中文版
作者:Chris Sells
译者:荣耀
【译注:以下是本译文中出现的部分名词术语英中对照表。但这并不意味我就一定将其译成中文,视乎语境,有时保留英文原词不译;并且,也不意味我以后一定这样翻译。】
英文 |
本文译法 |
说明 |
其它一些译法 |
managed |
托管的 |
参照VS Beta2 |
受控的、受管制的 |
unmanaged |
非托管的 |
参照VS Beta2 |
不受控的、不受管制的 |
attribute |
特性 |
|
属性、属性信息 |
property |
属性 |
此译法由来已久 |
特性 |
box(boxing) |
装箱 |
|
|
unbox(unboxing) |
拆箱 |
|
开箱 |
delegate |
委托 |
|
代理、代表 |
assembly |
配件 |
VS Beta2译为“程序集” |
程序集、组合体、组件、部件 |
constructor |
构造器 |
|
构造函数 |
destructor |
析构器 |
|
析构函数 |
exception |
异常 |
|
例外 |
namespace |
名字空间 |
|
名称空间 |
indexer |
索引器 |
|
索引函数 |
reflection |
反射 |
|
映射 |
CLR(common language runtime) |
公共语言运行时 |
|
公用语言运行期 |
garbage collection |
垃圾收集 |
|
无用资源回收 |
garbage collector |
垃圾收集器 |
|
无用资源回收程序 |
stack |
栈 |
有个别新手并不清楚“堆栈”到底指“堆”还是“栈” |
堆栈 |
heap |
堆 |
|
|
【摘要:如果你是一个C++老手,一开始你可能对向Visual Studio .NET迁移的念头警觉。然而,作为Visual C++的一个新特性—C++托管的扩展,为你提供了利用.NET框架创建应用的可能。当你使用managed C++时,你的代码被公共语言运行时(CLR)所管理,它提供了诸如垃圾收集、互操作能力以及更多的高级特性。本文解释了为什么你将愿意使用托管的扩展,怎样使用它们,如何混用托管的和非托管的代码,以及你的托管的代码如何能够同用其它语言编写的程序互操作】
C++语言已经存在好长时间了。它首先由Bjarne Stroustrup于1983年开发,后来于1997年被批准为ANSI标准。在那14年里,为了满足多种平台程序员社团要求,C++发生了很大进化。然而,即使在C++标准被批准之前,微软就开始扩展Visual C++,他们在每一个新版编译器里都加入了扩展,以满足以Windows为中心的程序员的特别需求。现在,随着Microsoft .NET的推广,微软C++编译器小组再一次使得C++程序员能够使用他们的语言去创建和使用新平台上的组件。然而,注意:这已不是你父亲时代的C++了,事情正在变得不同。
(还要注意的是,本文是基于可公开获得的.NET Framework SDK Beta1和Visual Studio .NET Beta1的。尽管概念应该相同,但.NET发行版和现在的版本肯定有变化。)
.NET精华
作为一名Windows开发人员,除了了解新的.NET平台之外,别的什么也帮不了你。仅仅作为一个复习,.NET主要特性有:
1.具有丰富类型支持的简便的语言互操作能力,包括对跨语言继承、类型异常、构造器和公共基类的支持。
2.垃圾收集,提供最优化的自动内存管理。
3.强健的版本管理,同一组件的多种版本可以和平共处于同一机器上或同一进程中。
4.微软中间语言MSIL使得代码可以得到验证和重定向。
这些特性都为.NET CLR所实现,它以独立组件方式提供了这些服务。(CLR是一个执行.NET组件的DLL。)组件自身包含元数据和实现,它是处理器相关的代码和IL的混合物。元数据提供了对语言互操作能力、垃圾收集、版本管理的支持,同时还支持IL和可验证性。
C#就是为了从根本上支持.NET所有主要特性而发明的,因此,对于它能够自然地处理它们也就不足为奇。例如,一个简单的C#类看起来如下:
// talker.cs
namespace MsdnMagSamples
{
public class Talker
{
public string Something = "something";
public void SaySomething() {System.Console.WriteLine(Something);}
}
}
可以这样编译该类(在一个名为talker.cs的文件里):
c:\> csc /t:library /out:talker.dll talker.cs
你会注意到C#酷似C++,除了一个非常重要的区别外:一旦我将类Talker编译成一个.NET配件(assembly)(粗略地说,是一个暴露.NET组件的DLL或EXE),将这个组件暴露给对其感兴趣的.NET客户(无论这些客户用何种语言实现)的全部必要条件就是在类前面加上一个public关键字,无需特定的入口点;也不必在.NET类型和C#类型之间做什么映射,被编译的组件提供了.NET运行时暴露类Talker所需要的所有元数据。例如,Visual Basic .NET客户可以如下方式使用元数据来访问类Talker:
'talkercli.vb
Public Module TalkerClient
Sub Main()
Dim t as new MsdnMagSamples.Talker
t.Something = "Hello, World"
t.SaySomething()
System.Console.WriteLine("Goodnight, Moon")
End Sub
End Module
可用如下命令行方式编译这个talkercli.vb文件:
c:\> vbc /t:exe /out:talkercli.exe /r:talker.dll talkercli.vb
相形之下,就这么一个简单的移植—将C#类Talker移植为C++ DLL,就甚至不能让其它C++客户去访问它(假如没有一些编译器技巧和前提的话),更不用说其他语言的客户了。不幸的是,C++程序员对这个特定限制早已见怪不怪。可以论证的是,整个Windows编程的历史就可以视为努力将一种语言编写的组件暴露给另一种语言客户的编年史。DLL使用C风格的函数,COM使用接口。对于C++程序员来说,实现DLL入口点不是一件舒服的事情,因为它感觉起来不够面向对象;另一方面,COM过量使用了面向对象,但除了接口外,没有什么类型是标准化的(译注:此句可疑),即使向COM客户暴露哪怕最简单的功能都会为C++程序员带来巨大的代码负担。
就象对DLL和COM所做的一样,微软正持续进行多语言支持,他们向自己的语言里加入了对.NET的支持,并鼓励其他语言卖主也如此。你应能指望看到你的微软旧爱,如Visual Basic、C++和Jscript,以及大约两打的第三方和研究性的语言,将支持.NET。实际上,在上次PDC(译注:(微软)职业开发者会议)上,我就坐在一个正把APL移植到.NET上的家伙的旁边。假如.NET连APL都打算支持了,你差不多可以确信你选择的语言也会跑到那儿了。
Managed C++客户
当然,我所选择的语言是C++,微软使用一种名为C++托管的扩展的东西以在.NET上支持C++,更广为人知的说法是managed C++。在managed C++中,使用托管的扩展生成的代码和组件被CLR所管理,它们会被垃圾收集,具备版本管理功能,并能够访问其它托管的类型,等等。例如,一个简单的managed C++程序可以访问C# 类Talker,如你在表1所见。可以这样编译该类:
C:\> cl /CLR talkcli.cpp /link /subsystem:console
表1 访问C# 类Talker // talkercli.cpp // Managed C++程序所必需 #using <mscorlib.dll> // 引入类Talker #using <talker.dll> using namespace MsdnMagSamples; void main() { Talker* t = new Talker(); t->Something = S"Hello, World"; t->SaySomething(); System::Console::WriteLine(S"Goodnight, Moon"); } |
表2 新的managed C++关键字和指示符 |
|
关键字 |
描述 |
__abstract |
声明一个不可直接实例化的类,Managed 接口内在地为抽象的 |
__box |
为值类型创建一个引用类型的拷贝 |
__delegate |
可以绑定于实例方法或静态方法的“函数指针” |
__event |
A rallying point for delegate implementations interested in receiving callbacks |
__finally |
声明一个finally语句块 |
__gc |
声明一个托管的类型 |
__identifier |
使得关键字作为一个标识符(在一个真正的多语言环境是必不可少的) |
__interface |
声明一个托管的接口 |
__nogc |
声明一个非托管的类型或一个指向托管的类型的非托管的指针 |
#pragma managed |
将一个代码范围声明为托管的,允许访问托管的类型。当以/CLR选项编译文件时,缺省即为托管的 |
#pragma unmanaged |
将一个代码范围声明为非托管的,并防止访问托管的类型 |
__pin |
防止一个托管的对象被垃圾收集器移走。当你在非托管的函数里调用托管的函数时,有时这是必需的。 |
__property |
为一个托管的类声明一个属性成员 |
__sealed |
防止将一个类或方法用作一个基类(方法)。值类型内在的为密封的 |
__try_cast |
若转换非法,抛出一个类型为System::IllegalCastException的异常 |
#using |
导入配件元数据 |
__value |
声明一个托管的值类型 |
这个客户例子引出了几个关于managed C++有趣的要点。首先,注意这个新的#using指示符的使用。(表2是一个managed C++关键字和指示符的完整列表)。第一个#using指示符告诉managed C++编译器引入描述所有核心.NET类型的元数据。这些类型包含在顶级System名字空间中,该名字空间包含了大量的嵌套类和名字空间,后者将.NET框架进行分级、分类。Managed C++中的名字空间和C++中的一样,因此,你不会对System::Console::WriteLine是调用嵌套于System名字空间中Console类的静态WriteLine方法感到惊讶。第二个#using指示符是引入我的自定义C#组件。在这里,我还使用了一个“using namespace”语句,当我访问类Talker时就可以节省一些打字输入,正如你在unmanaged C++中习惯做的一样。
其次,你会注意到我使用main作为我的入口点。Managed C++程序依然是C++程序,因此控制台应用需要一个main。同样,一旦你通过#using引入了类Talker,它就和常规的C++一样,通过new来创建一个实例,设置属性,调用方法。唯一的区别是在字符串前面加上“S”来指明它们是托管的,而不是非托管的。Managed C++编译器也非常乐于处理非托管的字符串,它也会为你转换ANSI和Unicode,但这会损失一些效率,因此,我避免使用它。如果你有兴趣的话,可以使用.NET反汇编工具ILDASM,来看看使用S前缀和不使用S前缀时,talkcli.exe中生成的IL有何不同。通常,ILDASM是一个奇妙的.NET编程工具,你应该熟悉它。
第三,为了使用托管的扩展并使你的代码被托管,可使用编译器新的/CLR选项。如果你喜欢在Visual Studio .NET中都这样,你可以设置项目属性页上的“使用托管的扩展”选项。
一般来说,在managed C++中使用.NET类型看起来、感觉上都比使用COM和ATL更象使用C++本地类型。真正主要的区别是我还没提到的那一点:当你使用new来创建一个托管的类型时,发生了什么?为什么我没有烦心去调用delete(你以为我忘了吗)?
垃圾收集
几年前,我逃避了一些工作,做了一个试验。我写了一个原生的COM客户,它创建一个COM对象,访问一些接口,调用一些方法,并对结果做一些事情。当然,用这种方式,我不得不显式释放我所获取的资源,例如接口指针、BSTR等等。为了完成这个试验,我将我的代码移植为使用智能类型(知道如何管理它们拥有的资源的C++类),因此,我不必手工释放我的资源。通过使用象CComPtr和CComBSTR这样的智能类型,我能够将我的COM客户代码行数减少40%左右。当然,如果你喜欢,你可以通过单个数据点写任意行代码。但我想你会同意,在你写的C++代码里,资源管理代码占了显著比重。
有些走运的程序员用不着处理这类事情。某些语言,比如基于COM的脚本语言和.NET之前的某些版本的Visual Basic,使用引用计数来管理对象。每一个附加的引用都是加在对象上的另一把锁,当所有的引用都解除时,对象被销毁。不幸的是,引用计数有一个大问题—引用周期。因为刚刚交付一个相当大的基于COM的项目,我可以告诉你,引用周期的bug真成问题,且极难跟踪。坦白地说,很多人甚至不愿费心去找到这些bug,更不用说去修复它们了(本身是一项困难的工作)。因此,.NET没有走引用计数路线,微软选择了另一条路线:垃圾收集。
.NET CLR使用垃圾收集器周期性地遍历所有已创建对象的列表,让它们知道,如果他们不再需要,就调用它们的Finalize方法,并将内存返还给由之而来的托管的堆。当我在Talker客户中使用关键字new时,我从.NET 托管的堆上配置内存,这意味着我不必记住去调用delete。并且,因为对象不是通过引用计数来跟踪的,你也不必担心引用周期的问题。利用.NET和垃圾收集,微软已经从你组件里的潜在的bug列表里,将内存泄漏和引用周期方面的bug拿走了。
资源管理
然而,垃圾收集器在玩弄其魔法时有个问题。使用智能类型或基于栈的对象的C++程序员习惯于认为对象在作用域边界被释放掉,对于托管的类型,将不再如此。
如果对象所关联的资源只是基于内存的(memory-based),这不成问题。当内存告急时,垃圾收集器当然会动作起来(通常这个动作在此之前早就发生了)。然而,如果你的对象中持有象文件句柄、数据库连接和socket连接等不是基于内存的资源,这些资源无法被确定性的自动收回。换句话说,即使垃圾收集器可能在将来某个时间调用你托管的对象的Finalize方法,你也不知道它究竟发生于何时(除非你利用System::GC::Collect强迫使其发挥作用,这会导致所有对象都被垃圾收集,而不单单是你正在处理的那一个对象)。假如持有的资源不久还会被使用,你也不能坐等垃圾收集器运行。相反,持有这种资源的对象的客户必需使对象知道手工释放这些资源。
正因如此,大多数携带非内存资源的托管的类型均实现一个称为Disposer模式的东西。Disposer模式要求托管的类型的客户在其完结时调用一个特定方法,通常称为Close或Dispose方法。为了在发生异常时也能够支持这种模式,managed C++对try-catch语句块提供了一个__finally扩展,它可以保证不管是否发生了异常,都会被调用:
void WriteToLog(const char* psz)
{
MyManagedFile* file;
try
{
file = new MyManagedFile("log.txt");
file->WriteLine(psz);
}
__finally
{
if(file) file->Close();
}
}
如果你是一名C++程序员,我猜测你对此第一反应是“变态”。你第二个反应可能会问,“为什么不在栈上创建这个对象让析构器去调用Close?”不幸的是,托管的类不能够创建在栈上。它们必需被创建在托管的堆上,这样垃圾收集器才能够管理它们。.NET有些称为值类型的东西可以被配置在栈上,但不幸的是,它们不能有析构器。实际上,托管的类型压根就没有C++意义上的析构器(就是那个在对象离开其作用域时被调用的方法)。它们只有Finalize方法,不管何时,只要垃圾收集器喜欢,它们就被调用(这也是首先导致此问题之所在)。
如果你对此问题的第三个反应是摆摆手然后又回到非托管的类型,在你离开之前,谢谢你的幸运星—你选择了托管的C++而不是C#或Visual Basic .NET,因为后二者都无法避免编写为我所描述的缺点所累的代码。然而,managed C++的便利之处在于它还能够使你在同一个文件里混合使用托管的和非托管的类型。你可以创建一个非托管的智能类型,它可以为你终结托管的类型。实际上,你将会使用微软在.NET SDK中提供的称为gcroot(定义在gcroot.h)的东西:
template <class T> struct gcroot {...};
类gcroot是一个非托管的模板类,它可以用来缓存一个指向托管的类型的指针。(.NET还不支持托管的模板)。当你希望在一个非托管的类型里缓存一个托管的类型作为成员数据时,这尤其有用。(managed C++并未提供对此直接支持。)类gcroot使用一个称为System::Runtime::InteropServices::GCHandle的托管的类在一个托管的指针和一个整数之间来回控制,这是gcroot如何能缓存它的原因。类gcroot还提供了一个操作符->来暴露托管的指针。然而,它的析构器并没有调用Finalize以让对象知道你已经完结。你可以使用gcroot的一个特化实现来达到这个目的,就象下面所示:
template <typename T>
struct final_ptr : gcroot<T>
{
final_ptr() {}
final_ptr(T p) : gcroot<T>(p) {}
~final_ptr()
{
T p = operator->();
if(p) p->Finalize();
}
};
这个类只是一个原型,在.NET平台发行之前,它可能还会变化。可在MSDN在线开发者中心(http://msdn.microsoft.com/net/)检查其升级情况。
使用这个类可将客户代码精简如下:
void WriteToLog(const char* psz)
{
final_ptr<MyManagedFile*> file = new MyManagedFile("log.txt");
file->WriteLine(psz);
}
编写和使用这种助手类的能力,展示了managed C++的威力,和诸如C#和Visual Basic .NET这样相对简单的语言相比,后两者需要编写更多的复杂代码来处理非内存资源。
托管的类和接口
当你使用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。