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这样相对简单的语言相比,后两者需要编写更多的复杂代码来处理非内存资源。
本文地址:http://com.8s8s.com/it/it46129.htm