M.E. Bring .NET CLR Support to C++中文版(下篇)

Managed Extensions Bring .NET CLR Support to C++中文版(下篇)<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

作者: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 <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。

-全文完-

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章