程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effecitve C#原則46:最小化與其它非托管代碼的交互

Effecitve C#原則46:最小化與其它非托管代碼的交互

編輯:關於C#

在開發設計.Net時,MS所做的最聰明的修改之一就是他們意識到,如果沒有 辦法整合已經存在的代碼到新的.Net環境中,那沒沒有人會接受這個新的平台。 MS知道,如果沒有辦法來利用已經存在的代碼,這將阻止大家接受它。與其它非 托管代碼的交互是可以工作了,但這是可交互唯一可以拿來說一下的有利的地方 。對於所有的交互策略,當操作流程在本地代碼和托管代碼之間的邊界上進行傳 送時,都要求強制提供一些 編組的信號。同時,交互策略強迫開發人員必須手 動的申明所有調用參數(譯注:這裡是說你根本不知道參數的數據類型,很多時 間你都只能以int32的方式傳遞所有參數,例如文件句柄,字符串指針等幾乎是 所有的參數,都只有一個int32也就是IntPtr類型進行處理,當然這裡認為是是 在32位機器上。)。最後,CLR還不能完成對托管代碼和本地代碼的邊界上進行數 據傳遞時的優化。忽略采用本地代碼或者COM對象時得到的好處吧,沒有什麼比 這更好的了(譯注:我本人強烈反對這一原則。C++,COM在目前來說,絕對有它 生存的優勢,我覺得應該充分利用這些優勢,而不應該忽略它們)。但事實是交 互並不是總能工作的,很多時候我們還是在要已經存在的應用程序中添加新的功 能,提高而且更新已經存在的工具,或者在其它的地方要完成一個新的托管應用 程序與舊的應用程序交互。使用一些交互在實際應用中只會是減緩對舊系統的更 替。所以,有明白不同的交互策略之間有什麼開銷是很重要的。這些開銷要同時 花在開發計劃以及運行時性能中。有些,最後的選擇是重寫舊代碼。還有一些時 候,你須要選擇正確的交互策略。

在我討論這個對你有用的交互策略之 前,我須要花一段來討論放棄(just throw it out)策略。第五章,與.Net框架 一起工作,向你展示了一些.Net裡已經為你創建好了的類和技術,你可以直接使 用或者派生。為你你想的很多,你可以確定一些類和你一些代碼算法,並且全部 用C#重寫。剩下存在的代碼可以被.Net框架裡已經存在的可能功能性的派生來取 代。這並不會總是在任何地方,任何時候都可以工作的,但這確實是一個經過認 真考慮過的遷移策略。整個第5章都推薦使用"throw it out“策略。 這一原則就專注於交互,而它確實是件痛苦的事情。

接下來,讓我們假 設你已經決定重寫全部代碼並不實際。一些不同的策略要求你從.Net中訪問本地 代碼。你須要明白在本地代碼和托管代碼的邊界上傳遞數據時的開銷的低效。在 使用交互時有三個開銷。首先就是數據集群處理,這在托管堆和本地代碼堆之間 進行數據傳遞時發生。其次就是在托管代碼和非托管代碼進行交互時的大量數據 吞吐時的開銷。你以及你的用戶要承擔這些開銷。第三個開銷就只是你自己的了 :你要在這個混合的開發環境中添加很多工作來實現交互。這也是最糟糕的一個 ,所以你的設計應該決定最小化這樣的開銷。

讓我們開始討論交互時在 性能上的開銷,以及如何最小化這些開銷。數據集群是最大的一個因數,就像是 網絡服務或者遠程操作一樣,你須要盡可能使用笨重的(chunky)API而不是小巧 的(chatty )API(譯注:數據集群是指你沒有辦法即時的與本地代碼進行交互, 而有一個延時,這個延時就使用數據堆集起來一起處理,這樣就使得你應該盡可 能少的與本地代碼進行交互,而要選擇一些一次可以處理較多數據的API)。你可 以用不同的方法來完成與非托管代碼的交互。你可以重新修改已經存在的非托管 代碼來創建一個笨重的API,更適合交互的API。常規的COM應用中是申明很多屬 性,這樣客戶可以設置並修改COM對象內部的狀態或者行為。每次的設置屬性都 會集群數據,而且不得不穿越邊界。(而且每在穿越交互邊界時也會有thunks。) 這非常的低效,不幸的是,COM對象或者非托管庫可能不受你控制。當這種情況 發生時,你須要完成更麻煩的工作。這時,你可以創建一個輕量級的C++庫,通 過使用你所須要的chunkier API來暴露類型的功能。這就要增加你的開發時間了 (也就是第三個開銷)。

當你封裝一個COM對象時,確保你修改的數據類型 已經在本地代碼一托管代碼之間提供了最好的數據集群策略。有些類型可以很好 的比其它類型進行集群,試著限制用於在本地代碼和托管代碼之間進行傳遞的數 據類型,盡量使用blittable數據。blittable是指托管代碼和本地代碼都一樣使 用的類型。數據內容可以直接拷貝而不用管對象的內部的結構。某些情況下,非 托管代碼可能使用托管代碼的代碼。下面列出了blittable 類型:

System.Byte
System.SByte
System.Int16
System.UInt16
System.Int32
System.UInt32
System.Int64
System.UInt64
System.UIntPtr

另 外,任何的blittable類型的一維數組也是blittable類型。最後,任何格式化的 包含blittable類型的也是blittable類型。一個格式化的類型可以是一個用 StructLayoutAttribute明確定義了數據層次的結構,

[ StructLayout( LayoutKind.Sequential ) ]
public struct Point3D
{
 public int X;
 public int Y;
 public int Z;
}

當你在托管代碼和非托管代碼之間,只使用blittable 類型 時,你就最小化了多少必須拷貝的信息呀!你同樣也優化了任何必須發生的拷貝 操作。

如果在數據拷貝時,你不能限制數據類型讓它成為blittable 類 型,你可以使用InAttribute 和OutAttribute 來進行控制。也COM類似,這些特 性控制數據拷貝的方法。In/Out 參數雙向拷貝,In參數以及Out參數是一次拷貝 。確保你應用嚴格限制的In/Out組合,來避免更多不必須拷貝。

最後, 你可以通過申明如何集群數據來提高性能。對於字符串來說這是最常見的。集群 字符串默認是使用BSTRs。這是一個安全的策略,但這也是最低效的。你可以通 過修改默認的集群格式減少額外的拷貝操作,可以使用MarshalAs 特性來修改集 群方式。下面申明了字符串的集群使用LPWStr或者wchar*:

public void SetMsg(
 [ MarshalAs( UnmanagedType.LPWStr ) ] string msg );

這有一個關於處理托管和非托管層上數據的轶事:數據被拷貝 然後在托管和非托管類型之間進行傳輸。你有三個方法業最小化拷貝操作。首先 就是通過限制參數和返回值,讓它們是blittable類型。這應該是首選的。當你 不能這樣做時,應用In和Out特性來最小化必須完成的拷貝和傳輸操作。最後一 個優化就是,很多類型可以不只一種集群方式,你應該選擇最優化的一種。

現在讓我們轉到如何在托管的非托管組件中轉移程序控制。你有三種選 擇:COM交互,平台調用(P/Invoke),以及托管C++。每種方法有它自己的優勢和 劣勢。

COM交互是最簡單的方法來使用已經存在的COM組件。但COM交互也 是在.Net中和本地代碼交互時最低效的一種方式。除非你的COM組件已經有很重 要的利益,否則不要這樣做。不要看也不要想這種方式。如果你沒有COM組件而 要使用這種方法就意味著你要把COM和交互原則學的一樣好。沒時間讓你開始理 解IUnknown(譯注:COM原理中最基本的接口,任何COM都實現了這樣的接口)。那 些這樣做的人都試著從我們的內存中盡快的清理它們。使用COM交互同樣意味著 在運行時你要為COM子系統承擔開銷。你同樣還要考慮,在不同的CLR對象的生命 期管理和COM版本的對象生命期管理之間又意味看什麼。你可以服從CLR的原則, 這就是說做一個你所導入的COM對象有一個析構函數,也就是在COM接口上調用的 Release()。或者你可以自己清楚的使用ReleaseCOMObject()來釋放COM對象。第 一種方法會給應用程序引入運行時的低效(參見原則15)。而第二個在你的程序裡 又是頭疼的事。使用ReleaseCOMObject ()就意味看你要深入到管理細節上,而 這些CLR的COM交互已經幫我們完成了。你已經了解了,而且你認你明白最好的。 CLR請求所有不同,而且它要釋放COM對象,除非你正確的告訴它,你已經完成了 。這是一個極大的鬼計,因為COM專家程序員都是在每個接口上調用Release(), 而你的托管代碼是以對象處理的。簡單的說,你必須知道什麼接口已經在對象上 添加了AddRef,而且只釋放這些(譯注:COM的引用非常嚴格,每個引用都會添加 一個AddRef,釋放時必須明確的給出Release(),而且必須成對使用,而在.Net 對COM進行封裝後,很多時候就是這個引用不匹配而出現資源洩漏)。就讓CLR來 管理COM的生命期,你來承擔性能損失就行了。你是一個煩忙的開發人員,想 在.Net中混合COM資源到托管環境裡,你要學習的太多了(也就是第三個開銷)。

第二個選擇是使用P/Invoke。這是一個更高效的方法來調用Win32的API ,因為你避免了在上層與COM打交道。而壞消息是,你須要為你使用的每個 P/Invoke方法手寫這些接口。越是調用多的方法,越是多的申明必須手寫。這種 P/Invoke申明就是告訴CRL如何訪問本地方法。這裡額外的解釋一下為什麼每個 一個關於P/Invoke的例子裡(也包括下面這個)都使用 MessageBox:

public class PInvokeMsgBox
{
  [ DllImport( "user32.dll" ) ]
  public static extern int MessageBoxA(
   int h, string m, string c, int type );
  public static int Main()
  {
   return MessageBoxA( 0,
    "P/InvokeTest",
    "It is using Interop", 0 );
  }
}

另一個使用 P/Invoke的主要缺點是,這並不是設計成面向對象的語言。如果你須要導入C++ 庫,你必須在你的導入申明中指明封裝名。假設取代Win32的MessageBox API, 你想訪問MFC的C++DLL裡的另外兩個AfxMessageBox 方法。你須要為其中一個方 法創建一個P/Invoke申明:

?AfxMessageBox@@YGHIII@Z

?AfxMessageBox@@YGHPBDII@Z

這兩個申明名是與下面的兩個方法匹配的:

int AfxMessageBox( LPCTSTR lpszText,
 UINT nType, UINT nIDHelp );
int AFXAPI AfxMessageBox( UINT nIDPrompt,
 UINT nType, UINT nIDHelp);

即使是在重寫少數幾個方法之後,你很快就明白這不 是一個高產的方法來提供交互功能。簡單的說,使用P/Invoke只能訪問C風格的 Win32方法(在開發上要開銷更多的時間)。

最後一種選擇就是在 Microsoft C++編譯器上使用/CLR開關來混合托管的非托管代碼。如果你編譯你 所有的本地代碼使用/CLR,你就創建了一個基於MSIL的庫,該庫使用本地堆來存 儲所有的數據。這就是說,這樣的C++庫不能直接被C#調用。你必須在你所熟悉 的代碼上創建一個托管的C++庫,用於在托管和非托管類型之間創建一個橋梁, 提供在托管和非托管的堆之間的數據集群支持。這樣的C++庫包含托管類,這些 數據成員是基於托管堆的。這些類同樣包含對本地對象的引用:

// Declare the managed class:
public __gc class ManagedWrapper : public IDisposable
{
private:
  NativeType* _pMyClass;
public:
 ManagedWrapper( ) :
   _pMyClass( new NativeType( ) )
 {
 }
 // Dispose:
 virtual void Dispose( )
 {
  delete _pMyClass;
  _pMyClass = NULL;
  GC::SuppressFinalize( this );
 }
 ~ManagedWrapper( )
 {
  delete _pMyClass;
 }
 // example property:
 __property System::String* get_Name( )
 {
  return _pMyClass- >Name( );
 }
 __property void set_Name( System::String* value )
 {
  char* tmp = new char [ value->Length + 1 ];
  for (int i = 0 ; i < value->Length; i++ )
    tmp[ i ] = ( char )value->Chars[ i ];
  tmp[ i ] = 0;
  _pMyClass->Name( tmp );
  delete [] tmp;
 }
 // example method:
 void DoStuff( )
 {
   _pMyClass->DoStuff( );
 }
 // other methods elided...
}

 

再說一次,這並不是一個像以前使用過的 高產的程序開發工具。這只是代碼重復,而且整個目的都要在托管和非托管數據 之間進行集群數據和thunking。優勢是,你可以從你的本地代碼中暴露出來的方 法和屬性上完成控制。而劣勢是你必須手寫一部份.Net代碼以及一部份C++代碼 。在這兩種代碼之間轉換很容易讓你發生錯誤。你還不能忘記刪除非托管對象。 托管對象不是你 要負責的。這會讓你的開發進度降下來,因為要不斷的檢測這 是否正確。

使用 /CLR 開關聽上去像是一個神話,但對於所有的交互來 說,這並不是一個神彈(magic bullet)。C++裡的模板和異常處理與C#是大不相 同的。寫的很好很高效的C++並不一寫能轉化成最好的MSIL結構。更重要是,編 譯C++代碼時的 /CLR開關並不做確認。正如我前面所說的,這樣的代碼是使用本 地堆:它訪問本地內存。而CLR並不能驗證這些代碼是安全的。調用這些代碼的 程序必須確保有安全許可來訪問不安全代碼。雖然如此,/CLR策略還是最好的一 個方法,在.Net中利用已經存在的C++代碼(不是COM對象)。你的程序不會招致 thunking開銷,而為你的C++庫現在並不在MSIL中,MSIL不是本地的CPU指令。

交互操作是件頭疼的事。在你使用交互之間,認真的考慮寫本地應用程 序。這經常是簡單而且很快的。不幸的是,對於很多的開發人員來說,交互是必 須的。如果你有已經存在的用其它語言寫的COM對象,使用COM交互。如果你有已 經存在的C++代碼,使用 /CLR 開關並托管C++來提供最好的策略來方法已經存在 的本地代碼。選擇最省時間的一個策略,這可能就是“just thro it out”策略。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved