C++/CLI:第一流的CLI語言
2005-08-25 11:25 作者: 朱先忠編譯 出處: 天極網 責任編輯:方舟
1. 簡介
本文并不是為了奉承C++/CLI的輝煌,也不是為了貶低其它如C#或者VB.NET等語言,相反,這只是一個非官方的、以一個喜歡這種語言的非微軟雇員身份來論證C++/CLI有它的自己的唯一的角色,可作為第一流的.NET編程語言。
一個不斷在新聞組和技術論壇上出現的問題是,當象C#和VB.NET這樣的語言更適合于這種用途時,為什么要使用C++來開發(fā).NET應用軟件。通常這樣一些問題后面的評論說是,C++語法是怎樣的復雜和令人費解,C++現在是怎樣一種過時的語言,還有什么VS.NET設計者已不再像支持C#和VB.NET一樣繼續(xù)支持C++。其中一些猜疑是完全荒謬的,但有些說法部分正確。希望本文有助于澄清所有這些圍繞C++/CLI語言及其在VS.NET語言層次中的地位的疑惑,神秘和不信任。請記住,本作者既不為微軟工作也沒有從微軟那里取得報酬,只是想從技術上對C++/CLI作一評判。
2. 快速簡潔的本機interop
除了P/Invoke機制可用在另外的象C#或VB.NET這樣的語言外,C++提供了一種獨有的interop機制,稱作C++ interop。C++ interop比P/Invoke直觀得多,因為你只是簡單地#include需要的頭文件,并與需要的庫進行鏈接就能象在本機C++中一樣調用任何函數。另外,它比P/Invoke速度快--這是很容易能證明的?,F在,可爭辯的是在實際應用軟件的開發(fā)中,經由C++ interop獲得的性能好處與花在用戶接口交互、數據庫存取、網絡數據轉儲、復雜數學算法等方面的時間相比可以被忽略,但是事實是在有些情況下,甚至通過每次interop調用節(jié)省的幾個納秒也能給全局應用程序性能/響應造成巨大影響,這是絕對不能被忽視的。下面有兩部分代碼片斷(一個是使用P/Invoke機制的C#程序,一個是使用C++ Interop機制的C++程序),我分別記錄了其各自代碼重復執(zhí)行消耗的時間(毫秒)。不管你如何解釋這些數據,不管這會對你的應用程序產生什么影響,全是你的事。我僅打算事實性地指出,C++代碼的執(zhí)行速度要比C#(其中使用了較多的本機interop調用)快。
1) C#程序(使用P/Invoke)
[SuppressUnmanagedCodeSecurity] [DllImport("kernel32.dll")] static extern uint GetTickCount(); [SuppressUnmanagedCodeSecurity] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern uint GetWindowsDirectory( [Out] StringBuilder lpBuffer, uint uSize); static void Test(int x) { StringBuilder sb = new StringBuilder(512); for (int i = 0; i < x; i++) GetWindowsDirectory(sb, 511); } static void DoTest(int x) { uint init = GetTickCount(); Test(x); uint tot = GetTickCount() - init; Console.WriteLine("Took {0} milli-seconds for {1} iterations",tot, x); } static void Main(string[] args) { DoTest(50000);DoTest(500000);DoTest(1000000);DoTest(5000000); Console.ReadKey(true); } |
2) C++程序(使用C++ Interop)
void Test(int x) { TCHAR buff[512]; for(int i=0; i<x; i++) GetWindowsDirectory(buff, 511); } void DoTest(int x) { DWORD init = GetTickCount(); Test(x); DWORD tot = GetTickCount() - init; Console::WriteLine("Took {0} milli-seconds for {1} iterations",tot, x); } int main(array<System::String ^> ^args) { DoTest(50000);DoTest(500000);DoTest(1000000);DoTest(5000000); Console::ReadKey(true); return 0; } |
3) 速度比較
重復次數 |
C# 程序 |
C++程序 |
50,000 |
61 |
10 |
500,000 |
600 |
70 |
1,000,000 |
1162 |
140 |
5,000,000 |
6369 |
721 |
其性能差別真是令人驚愕!這的確是說明為什么要使用C++/CLI的一個好理由,如果你在使用本機interop進行開發(fā),那么性能!完全由于性能,我就將被迫借助本機interop來實現并非基于web的.NET應用程序。當然,為什么我想要使用.NET來開發(fā)需要大量本機interop技術的應用程序完全是另外一個問題。
如果你仍懷疑這種性能優(yōu)勢,有另外的理由來說明你為什么不得不使用C++/CLI而不是C#或VB.NET——源碼膨脹!下面是一個C++函數的例子,它使用了IP幫助者API來枚舉一臺機器上的網絡適配器并且列出與每個適配器相聯系的所有IP地址。
4) 枚舉n/w適配器的C++代碼
void ShowAdapInfo() { PIP_ADAPTER_INFO pAdapterInfo = NULL; ULONG OutBufLen = 0; //得到需要的緩沖區(qū)大小 if(GetAdaptersInfo(NULL,&OutBufLen)==ERROR_BUFFER_OVERFLOW) { int divisor = sizeof IP_ADAPTER_INFO; #if _MSC_VER >= 1400 if( sizeof time_t == 8 ) divisor -= 8; #endif pAdapterInfo = new IP_ADAPTER_INFO[OutBufLen/divisor]; //取得適配器信息 if( GetAdaptersInfo(pAdapterInfo, &OutBufLen) != ERROR_SUCCESS ) {//調用失敗 } else { int index = 0; while(pAdapterInfo) { Console::WriteLine(gcnew String(pAdapterInfo->Description)); Console::WriteLine("IP Address list : "); PIP_ADDR_STRING pIpStr = &pAdapterInfo->IpAddressList; while(pIpStr) { Console::WriteLine(gcnew tring(pIpStr->IpAddress.String)); pIpStr = pIpStr->Next; } pAdapterInfo = pAdapterInfo->Next; Console::WriteLine(); } } delete[] pAdapterInfo; } } |
現在讓我們看一個使用P/Invoke的C#版本。
5) 使用P/Invoke技術的C#版本
const int MAX_ADAPTER_NAME_LENGTH = 256; const int MAX_ADAPTER_DESCRIPTION_LENGTH = 128; const int MAX_ADAPTER_ADDRESS_LENGTH = 8; const int ERROR_BUFFER_OVERFLOW = 111; const int ERROR_SUCCESS = 0; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADDRESS_STRING { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string Address; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADDR_STRING { public IntPtr Next; public IP_ADDRESS_STRING IpAddress; public IP_ADDRESS_STRING Mask; public Int32 Context; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADAPTER_INFO { public IntPtr Next; public Int32 ComboIndex; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ADAPTER_NAME_LENGTH + 4)] public string AdapterName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ADAPTER_DESCRIPTION_LENGTH + 4)] public string AdapterDescription; public UInt32 AddressLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_ADAPTER_ADDRESS_LENGTH)] public byte[] Address; public Int32 Index; public UInt32 Type; public UInt32 DhcpEnabled; public IntPtr CurrentIpAddress; public IP_ADDR_STRING IpAddressList; public IP_ADDR_STRING GatewayList; public IP_ADDR_STRING DhcpServer; public bool HaveWins; public IP_ADDR_STRING PrimaryWinsServer; public IP_ADDR_STRING SecondaryWinsServer; public Int32 LeaseObtained; public Int32 LeaseExpires; } [DllImport("iphlpapi.dll", CharSet = CharSet.Ansi)] public static extern int GetAdaptersInfo(IntPtr pAdapterInfo, ref int pBufOutLen); static void ShowAdapInfo() { int OutBufLen = 0; //得到需要的緩沖區(qū)大小 if( GetAdaptersInfo(IntPtr.Zero, ref OutBufLen) == ERROR_BUFFER_OVERFLOW ) { IntPtr pAdapterInfo = Marshal.AllocHGlobal(OutBufLen); //取得適配器信息 if( GetAdaptersInfo(pAdapterInfo, ref OutBufLen) != ERROR_SUCCESS ) { //調用失敗了 } else{ while(pAdapterInfo != IntPtr.Zero) { IP_ADAPTER_INFO adapinfo = (IP_ADAPTER_INFO)Marshal.PtrToStructure( pAdapterInfo, typeof(IP_ADAPTER_INFO)); Console.WriteLine(adapinfo.AdapterDescription); Console.WriteLine("IP Address list : "); IP_ADDR_STRING pIpStr = adapinfo.IpAddressList; while (true){ Console.WriteLine(pIpStr.IpAddress.Address); IntPtr pNext = pIpStr.Next; if (pNext == IntPtr.Zero) break; pIpStr = (IP_ADDR_STRING)Marshal.PtrToStructure( pNext, typeof(IP_ADDR_STRING)); } pAdapterInfo = adapinfo.Next; Console.WriteLine(); } } Marshal.FreeHGlobal(pAdapterInfo); } } |
3. 棧語義和確定性的析構
C++經由棧語義模仿給了我們確定性的析構。簡言之,棧語義是Dispose模式的良好的語法替代品。但是它在語義上比C# using塊語法更直觀些。請看下列的C#和C++代碼段(都做一樣的事情-連接兩個文件的內容并把它寫到第三個文件中)。
1) C#代碼--使用塊語義
public static void ConcatFilestoFile(String file1, String file2, String outfile) { String str; try{ using (StreamReader tr1 = new StreamReader(file1)) { using (StreamReader tr2 = new StreamReader(file2)) { using (StreamWriter sw = new StreamWriter(outfile)) { while ((str = tr1.ReadLine()) != null) sw.WriteLine(str); while ((str = tr2.ReadLine()) != null) sw.WriteLine(str); } } } } catch (Exception e) { Console.WriteLine(e.Message); } } |
2) C++代碼--棧語義
static void ConcatFilestoFile(String^ file1, String^ file2, String^ outfile) { String^ str; try{ StreamReader tr1(file1); StreamReader tr2(file2); StreamWriter sw(outfile); while(str = tr1.ReadLine()) sw.WriteLine(str); while(str = tr2.ReadLine()) sw.WriteLine(str); } catch(Exception^ e) { Console::WriteLine(e->Message); } } |
C#代碼與相等的C++ 代碼相比不僅免不了冗長,而且using塊語法讓程序員自己明確地指定他想在哪兒調用Dispose(using塊的結束處),而使用C++/CLI的棧語義,只需讓編譯器使用常規(guī)的范圍規(guī)則來處理它即可。事實上,這使得在C#中修改代碼比在C++中更乏味-作為一實例,讓我們修改這些代碼以便即使僅存在一個輸入文件也能創(chuàng)建輸出文件。請看下面修改后的C#和C++代碼。
3) 修改后的C#代碼
public static void ConcatFilestoFile(String file1, String file2, String outfile) { String str; try{ using (StreamWriter sw = new StreamWriter(outfile)) { try{ using (StreamReader tr1 = new StreamReader(file1)) { while ((str = tr1.ReadLine()) != null) sw.WriteLine(str); } } catch (Exception) { } using (StreamReader tr2 = new StreamReader(file2)) { while ((str = tr2.ReadLine()) != null) sw.WriteLine(str); } } } catch (Exception e){ } } |
把針對StreamWriter的using塊放到頂層需要重新調整using塊結構--這在上面情況下顯然不是個大問題,但是對于實際開發(fā)中的修改,這可能是相當模糊的且易導致邏輯錯誤的。
4) 修改后的C++代碼
static void ConcatFilestoFile(String^ file1, String^ file2, String^ outfile) { String^ str; try{ StreamWriter sw(outfile); try{ StreamReader tr1(file1); while(str = tr1.ReadLine()) sw.WriteLine(str); } catch(Exception^){} StreamReader tr2(file2); while(str = tr2.ReadLine()) sw.WriteLine(str); } catch(Exception^){} } |
這樣不是比在C#中的做更容易些嗎?我恰好把StreamWriter聲明移到了頂部并增加了一個額外的try塊,就這些。甚至對于象在我的示例代碼片斷中的瑣碎事情,如果所涉及的復雜性在C++中大大減少,那么,當你工作于更大的工程時你能想象使用棧語義對你的編碼效率千萬的影響。
還不確信?好,讓我們看一下成員對象和它們的析構吧。Imagine CLI GC類R1和R2,二者都實現了Idisposable接口且都有函數F(),還有一個CLI GC類R,它有R1和R2成員和一個函數F()-它內部地調用R1和R2上的F()成員函數。讓我們先看C#實現。
5) 一個disposable類繼承層次的C#實現
class R1 : IDisposable{ public void Dispose() { } public void F() { } } class R2 : IDisposable{ public void Dispose() { } public void F() { } } class R : IDisposable{ R1 m_r1 = new R1(); R2 m_r2 = new R2(); public void Dispose() { m_r1.Dispose(); m_r2.Dispose(); } public void F() { m_r1.F(); m_r2.F(); } public static void CallR() { using(R r = new R()) {r.F();} } } |
這里有幾件事情要做:必須為每個disposable 類手工實現IDisposable接口,對于具有成員R1和R2的類R,Dispose方法也需要調用成員類上的Dispose?,F在讓我們分析上面幾個類的C++實現。
6) 等價的C++實現
ref class R1 { public: ~R1(){} void F(){} }; ref class R2 { public: ~R2(){} void F(){} }; ref class R { R1 m_r1; R2 m_r2; public: ~R(){} void F() { m_r1.F(); m_r2.F(); } static void CallR() { R r; r.F(); } }; |
注意,這里不再有手工的Idisposable接口實現(我們的類中僅建立了析構器)而且最好的部分--類R的析構器(Dispose方法)并沒有在該類可能含有的可釋放的成員上調用Dispose-它沒有必要這樣做,編譯器自動為之生成所有的代碼!
4. 混合類型
我們知道,C++支持本機類型-總是如此;C++支持CLI類型-本文正是特別強調這一點;它還支持混合類型-具有CLI成員的本機類型和具有本機成員的CLI類型!請盡管考慮所有你能的可能需求。
注意,談到Whidbey,混合類型實現還不完整;就我從Brandon,Herb和Ronald發(fā)表的材料的理解得知,存在這種相當酷的類型--統一模型,它將在Orcas中實現--你能夠在本機C++堆上new/delete CLI類型,而且也能夠在CLI堆上gcnew/delete本機類型。但既然這是Whidbey以后的東西,本文不討論統一模型。
在我談論你何時使用混合類型以前,我想向你說明什么是混合類型。如果你理解混合類型,請?zhí)^下面幾段。這里引用Brandon Bray的說法:"一種混合類型,或者是本機類ref類(需要有對象成員),或者是通過聲明或繼承被分配在垃圾回收堆或本機堆上的。"因此如果你有一個托管類型或者有一個有托管成員的本機類型,你就有了一個混合類型。VC++ Whidbey不直接支持混合類型(統一類型模型是一種Whidbey之后的概念),但是它給我們劃定了實現混合類型的條件。讓我們開始討論包含托管成員的本機類型。
ref class R { public: void F(){} //假定 non-trivial ctor/dtor R(){} ~R(){} }; |
在我的例子中,設想該托管類型R有一個non-trivial構造器和一個non-trivial析構器。
class Native { private: gcroot<R^> m_ref; public: Native(): m_ref(gcnew R()){} ~Native() { delete m_ref; } void DoF() { m_ref->F(); } }; |
既然,我不能在我的類中擁有一個R成員,我使用了gcroot模板類(在gcroot.h中聲明,但是你要用"#include vcclr.h"),它包裝了System::Runtime::InteropServices::GCHandle結構。它是個象類一樣的靈敏指針,它重載了運算符->以返回用作模板參數的托管類型。因此在上面類中,我可以使用m_ref,就好象我已經聲明它是R^,而且你能在DoF函數中看到這正在起作用。實際上你可以節(jié)省delete,這可以通過使用auto_gcroot(類似于std::auto_ptr,在msclr\auto_gcroot.h文件中聲明)代替gcroot來實現。下面是一個更好些的使用auto_gcroot的實現。
class NativeEx { private: msclr::auto_gcroot<R^> m_ref; public: NativeEx() : m_ref(gcnew R()){} void DoF() { m_ref->F(); } }; |
下面讓我們看相反的情形:一個CLI類的本機成員。
ref class Managed { private: Native* m_nat; public: Managed():m_nat(new Native()){ } ~Managed() { delete m_nat; } !Managed() { delete m_nat; #ifdef _DEBUG throw gcnew Exception("Oh, finalizer got called!"); #endif } void DoF() { m_nat->DoF(); } }; |
我不能定義一個Native對象來作為一個ref類成員,因此需要使用一個Native*對象來代替。我在構造器中new該Native對象,然后在析構器和finalizer中delete它。如果你運行該工程的調試版,在執(zhí)行到finalizer時將拋出一個異常- 因此開發(fā)者可以馬上添加一個對delete的調用或為他的CLI類型使用棧語義技術。奇怪的是,庫開發(fā)小組沒有建立一個gcroot的反向實現-但這不是個大問題,我們可以自己寫。
template<typename T> ref class nativeroot { T* m_t; public: nativeroot():m_t(new T){} nativeroot(T* t):m_t(t){} T* operator->() { return m_t; } protected: ~nativeroot() { delete m_t; } !nativeroot() { delete m_t; #ifdef _DEBUG throw gcnew Exception("Uh oh, finalizer got called!"); #endif } }; |
這僅是個相當簡單的靈敏指針實現,就象一個負責本機對象分配/回收的ref類。不管怎樣,借助nativeroot模板類,我們可以如下修改托管類:
ref class ManagedEx { private: nativeroot<Native> m_nat; public: void DoF() { m_nat->DoF(); } }; |
好,關于混合類型的最大問題是什么呢?你可能問。最大問題是,現在你能混合使用你的MFC、ATL、WTL、STL代碼倉庫和.NET框架,并用可能的最直接的方式-只需寫你的混合模式代碼并編譯實現!你可以建立在一個DLL庫中建立MFC 類,然后建立一個.NET應用程序來調用這個DLL,還需要把.NET類成員添加到你的MFC類(也實現可以相反的情況)。
作為一例,設想你有一MFC對話框--它通過一個多行的編輯框接受來自用戶的數據-現在,你有一新的要求-顯示一個只讀編輯框,它將顯示當前在該多行編輯框中文本的md5哈希結果。你的隊友正在悲嘆他們將必須花費幾個小時鉆研crypto API,而你的上司在擔憂你們可能必須要買一個第三方加密庫;那正是你在他們面前樹立形象的時候,你宣布你將在15分鐘內做完這項任務。下面是解決的辦法:
添加一個新的編輯框到你的對話框資源中,并且添加相應的DDX變量。選擇/clr編譯模式并且添加下列代碼到你的對話框的頭文件中:
#include <msclr\auto_gcroot.h> using namespace System::Security::Cryptography; |
使用auto_gcroot模板來聲明一個MD5CryptoServiceProvider成員:
protected: msclr::auto_gcroot<MD5CryptoServiceProvider^> md5; |
在OnInitDialog過程中,gcnew MD5CryptoServiceProvider成員。
md5 = gcnew MD5CryptoServiceProvider(); |
并且為多行編輯框添加一個EN_CHANGE處理器:
void CXxxxxxDlg::OnEnChangeEdit1() { using namespace System; CString str; m_mesgedit.GetWindowText(str); array<Byte>^ data = gcnew array<Byte>(str.GetLength()); for(int i=0; i<str.GetLength(); i++) data[i] = static_cast<Byte>(str[i]); array<Byte>^ hash = md5->ComputeHash(data); CString strhash; for each(Byte b in hash) { str.Format(_T("%2X "),b); strhash += str; } m_md5edit.SetWindowText(strhash); } |
這里使用了混合類型:一個本機Cdialog派生類,該類含有一個MD5CryptoServiceProvider成員(CLI類型)。你可以輕易地試驗相反的情況(如早期的代碼片斷已顯示的)——可以建立一個Windows表單應用程序而且可能想利用一個本機類庫--這不成問題,使用上面定義的模板nativeroot即可。
5. 托管模板
也許你對泛型的概念已很清楚了,它幫助你避免進入C++的模板夢魘,它是實現模板的最佳方式,等等。好,假設這些全部正確,C++/CLI支持泛型就象任何其它CLI語言一樣-但是它有而其它一些CLI語言還沒有的是它還支持托管模板-也就是模板化的ref和value類。如果你以前從未使用過模板,你不能一下欣賞這么多優(yōu)點,但是如果你有模板使用背景而且你已發(fā)現了泛型中存在的可能限制你編碼的方式,托管模板將會大大減輕你的負擔。你能聯合使用泛型和模板- 事實上有可能用一個托管類型的模板參數來實例化一個泛型類型(盡管相反的情形是不可能的,因為運行時刻實例化由泛型所用)。STL.NET (或STL/CLR)以后討論,請很好地利用泛型和托管模板的混合編程吧。
泛型使用的子類型約束機制將防止你寫出下面的代碼:
generic<typename T> T Add(T t1, T t2) { return t1 + t2; } |
編譯錯誤:
error C2676: binary ’+’ : ’T’ does not define this operator or a conversion to a type acceptable to the predefined operator |
現在請看相應的模板版本:
template<typename T> T Add(T t1, T t2) { return t1 + t2; } |
那么就可以這樣做:
int x1 = 10, x2 = 20; int xsum = Add<int>(x1, x2); |
還可以這樣做:
ref class R { int x; public: R(int n):x(n){} R^ operator+(R^ r) { return gcnew R(x + r->x); } }; //... R^ r1 = gcnew R(10); R^ r2 = gcnew R(20); R^ rsum = Add<R^>(r1, r2); |
這在一個象int的本機類型以及一個ref類型(只要ref類型有一個+運算符)情況下都能工作良好。這個泛型缺點不是一個調試錯誤或缺陷-它是設計造成的。泛型的實例化是在運行時通過調用配件集實現的,因此編譯器不能確知一特定操作能被施行于一個泛型參數,除非它匹配一個子類型約束,因此編譯器在定義泛型時解決這個問題。當你使用泛型時的另外一個妨礙是,它不會允許你使用非類型參數。下列泛型類定義不會編譯:
generic<typename T, int x> ref class G{}; |
編譯錯:
error C2978: syntax error : expected ’typename’ or ’class’; found type ’int’; non-type parameters are not supported in generics |
與托管模板相比較:
template<typename T, int x = 0> ref class R{}; |
如果你開始感激C++向你提供了泛型和托管模板,那么請看下面這一個例子:
template<typename T> ref class R{ public: void F() { Console::WriteLine("hey"); } }; template<> ref class R<int> { public: void F() { Console::WriteLine("int"); } }; |
你不能用泛型這樣編碼;否則,將產生:
編譯錯:error C2979: explicit specializations are not supported in generics
但可以在繼承鏈中混合使用模板和泛型:
generic<typename T> ref class Base { public: void F1(T){} }; template<typename T> ref class Derived : Base<T> { public: void F2(T){} }; //... Derived<int> d; d.F1(10); d.F2(10); |
最后,你不能從一個泛型參數類型派生一個泛型類。
下列代碼不會成功編譯:
generic<typename T> ref class R : T {}; |
error C3234: a generic class may not derive from a generic type parameter
模板讓你這樣做(好像你還不知道這些):
ref class Base{ public: void F(){} }; generic<typename T> ref class R : T {}; //... R<Base> r1; r1.F(); |
這樣,當你下次遇到對泛型的貶謗時,你就知道該怎么做了。
6. STL/CLR
當大量使用STL的C++開發(fā)者轉向.NET1/1.1時一定感覺非常別扭,他們中的許多可能會放棄并轉回到原來的本機編碼。從技術上講,你能結合.NET類型(using gcroot)使用本機STL,但是產生的結果代碼可能相當低效,更不用說是丑陋了:
std::vector< gcroot<IntPtr> >* m_vec_hglobal; //... for each(gcroot<IntPtr> ptr in *m_vec_hglobal) { Marshal::FreeHGlobal(ptr);} |
大概VC++小組考慮到了這些并決定在Whidbey以后,他們會提供STL.NET(或STL/CLR)并可以單獨從網上下載。
你可能問為什么?Stan Lippman,在他的MSDN文章(STL.NET Primer)中給出了3條原因:
·可擴展性--STL設計把算法和容器隔離到自己的應用空間-也就是你可以有一組容器和一組算法,并且你能在任何一個容器上使用這些算法;同時你能在任何一個算法中使用這些容器。因此,如果你添加一種新的算法,你能在任何一種容器中使用它;同樣,一個新的容器也可以與現有算法配合使用。
·統一性--所有核心C++開發(fā)者集中在一起,匯集起他們精妙的STL專長,再使用他們的專長則輕車熟路。要較好地使用STL需要花費時間-然而一旦你掌握了它,你就有了在.NET世界中使用你的技巧的明顯優(yōu)勢。不是嗎?
·性能--STL.NET通過使用實現泛型接口的托管模板實現。并且既然它的核心已用C++和托管模板編碼,可以期盼它比在BCL上使用的泛型容器更具有性能優(yōu)勢。
使用過STL的人不需要任何示范,所以下面代碼有益于以前沒有使用過STL的人。
vector<String^> vecstr; vecstr.push_back("wally"); vecstr.push_back("nish"); vecstr.push_back("smitha"); vecstr.push_back("nivi"); deque<String^> deqstr; deqstr.push_back("wally"); deqstr.push_back("nish"); deqstr.push_back("smitha"); deqstr.push_back("nivi"); |
我使用了兩個STL.NET容器-vector和deque,并裝滿兩個容器,使其看起來相同(在兩個容器中都使用了push_back)?,F在,我將在兩個容器上使用replace算法-我們再次看到,這些代碼是很相同的。
replace(vecstr.begin(), vecstr.end(), gcnew String("nish"), gcnew String("jambo")); replace(deqstr.begin(), deqstr.end(), gcnew String("nish"), gcnew String("chris")); |
這里特別要注意的是我使用了"同樣"的算法--replace并在兩個不同STL容器上使用相同的函數調用。這是當Stan談及"可擴展性"時的意思。下面我用一個簡單函數來證明:
template<typename ForwardIterator> void Capitalize( ForwardIterator first,ForwardIterator end) { for(ForwardIterator it = first; it < end; it++) *it = (*it)->ToUpper(); } |
它遍歷一個System::String^容器并把其中的每個字符串轉化為大寫。
Capitalize(vecstr.begin(), vecstr.end()); Capitalize(deqstr.begin(), deqstr.end()); for(vector<String^>::iterator it = vecstr.begin(); it < vecstr.end(); it++) Console::WriteLine(*it); Console::WriteLine(); for(deque<String^>::iterator it = deqstr.begin(); it < deqstr.end(); it++) Console::WriteLine(*it); |
上面我的算法能夠與vector和deque容器工作良好。至此,不再細談;否則,guru站上的STL愛好者們會對我群起攻擊,而非STL人可能感到厭煩。如果你還沒使用過STL,可以參考有關資料。
7. 熟悉的語法
開發(fā)者經常迷戀他們所用的編程語言,而很少是出于實用的目的。還記得當微軟宣布不再為VB6提供官方支持時,VB6人的反抗嗎?非VB6人對此可能非常震驚,而老道的VB6人早已為他們的語言作好葬禮準備了。事實上,如果VB.NET從來沒被發(fā)明,多數VB6人將會離開.NET,因為C#將會對他們非常陌生,而它的祖先就是C++。如今,許多VB.NET人可能已經轉向了C#,但是他們不會從VB6直接轉向C#;VB.NET起到一個橋梁作用讓他們的思想脫離開原來VB6思想。相應地,如果微軟僅發(fā)行VB.NET(而沒有C#),那么.NET可能成為了新的面向對象VB,且?guī)в幸粋€更大的類庫-C++社團的人可能對此嗤之以鼻-他們甚至不會麻煩地檢驗.NET基礎類庫。為什么任何使用一種特定語言的開發(fā)者會對另外一個團體的使用另外開發(fā)語言的開發(fā)者嗤之以鼻?這不是我要回答的問題。--要回答該問題也許要先回答為什么有的人喜歡威士忌,有的人喜歡可口可樂,而還有人喜歡牛奶。所有我要說的是,對開發(fā)者來說,語法家族是個大問題。
你認為對于一個具有C++背景的人,下面的代碼具有怎樣的直覺性?
char[] arr =new char[128]; |
他/她可能回答的第一件事是,方括號放錯了位置。下面這句又如何?
"呀!"-最可能的反映?,F在把下面與前面相比較:
char natarr[128]; array<char>^ refarr=gcnew array<char>(128); int y=refarr->Length; |
請注意聲明一個本機數組和一個托管數組時的語法區(qū)別。這里不同的模板形式的語法可視化地告誡開發(fā)者這一事實--refarr并不是典型的C++數組而且它可能是某種CLI類的派生物(事實上確是如此),所以極有可能可以把方法和屬性應用于它。
C#的finalizer語法最有可能引起轉向C#的C++程序員的混淆。請看見下列C#代碼:
好,這樣~R看起來象一個析構器但實際是個finalizer。為什么?請與下面的C++代碼比較:
ref class R { ~R(){ } !R(){ } }; |
這里~R是析構器(實際上等價于一個析構器的Dispose模式-但對C++人員來說,這它的行為象個析構器)而新的!R語法是為finalizer建立的-這樣就不再有混淆而且語法看上去也與本機C++相匹配。
請看一下C#泛型語法:
再請看一下C++的語法:
generic<typename T> ref class R{}; |
曾經使用過模板的人馬上就看出這種C++語法,而C#語法不能保證其沒有混淆性且也不直觀。我的觀點是,如果你以前具有C++背景,C++/CLI語法將最貼近于你以前所用。C#(以及J#)看上去象C++,但是還有相當多的極為使人煩火的奇怪語義差別而且如果你沒有完全放棄C++,語法差別將永遠不停地帶給你混亂和挫折。從這種意義上說,我認為VB.NET更好些,至少它有自己唯一的語法,所以那些共用C++和VB.NET的人不會產生語法混亂。
8. 結論
最后,至于你用什么語言編程,這可能依賴許多因素——如:你在學校學習的是什么語言,你是用什么語言開發(fā)的現有代碼倉庫,是否你的客戶對你有具體的語言要求等。本文的主要目的是幫助你確定使用C++/CLI的幾個明確的場所--這里,它比另外CLI語言更具有明顯優(yōu)勢。如果你開發(fā)的應用程序有90%的使用時間是涉及本機interop,為何還要考慮使用另外的而不是C++?如果你想開發(fā)一個通用集合,為什么僅把你自己限制在泛型上,而不是結合泛型和模板的優(yōu)勢呢?如果你已經用C++工作,為什么還要學習一種新的語言?我常覺得象C#和VB.NET這樣的語言總是盡量向開發(fā)者隱藏CLR,而C++不僅讓你品味CLR,甚至可以讓你去親吻CLR!