Effective C#筆記(4)

這章主要講如何創建二進制組件(Component),組件的Assembly是爲了更容易共享組件裏面的邏輯,利用誇語言編程的功能,使得發佈更容易。減少兩個組件之間的的耦合度可以使得組件的發佈變得更容易。下面主要就介紹如何創建易用,易發佈,易更新的Assembly(Assmeblies)。
CLR加載Assembly是根據需要來的,只有使用到的Assembly纔會加載進內存裏面。首先,CLR會決定什麼文件需要加載,在Assembly的無數據中記錄了這個Assembly所依賴的其它Assembly,對於強命名的Assembly來說,會包括名字,版本,Culture,Key Token在裏面,而對於弱命名的Assembly,只有名字存在這個記錄裏面。使用強命名的Assembly可以防止惡意的軟件替換你的Assembly。對於強命名的Assembly,首先會在GAC(Global Assembly Cache)裏面查找是否已經存在,如果在配置文件裏面存在Codebase目錄,則會對這個目錄查找,如果該目錄裏面找不到,則加載這個Assembly失敗。如果沒有Codebase目錄,則會從當前應用程序目錄,Cluture子目錄,Assembly子目錄(Culture和Assembly子目錄是組合的,比如可以是culture下的Assembly子目錄,也可以是Assembly下的Culture子目錄)裏面查找。
由上面的描述可以知道,只有強命名的的Assembly可以存儲在GAC裏面,可能通過配置文件去修改Assembly更新的默認行爲,強命名的Assembly通過防止惡意的篡改(Malicious Tampering)具有更高的安全性。
因此,我們創建Assembly的時候,要儘量創建強命名的Assembly,並把元數據裏面的所有信息都填寫完整。
對於AssemblyCulture,只有當Assembly具有本地化的資源的時候,才需要填寫。
對於AssemblyVersion版本,可以採用默認的1.0.*,這樣編譯器會根據編譯的時間來生成後面的編譯版本(Build Version)和修改版本(Revision Version),這樣保證版本號是永遠向上增長的。但要注意對於COM的Assembly來說,不要採用編譯器自動生成的版本號,因爲每一個版本的COM的Assembly都會在註冊表裏面註冊一個記錄,這樣如果每個都修改版本號,會很快把註冊表塞滿。
對於強命名的Assembly來說,也有一個例外,就是對於ASP.NET的程序,強命名的Assembly加載得不正確(不知3.0修正了沒有),而且強命名的Assembly必須添加AllowParticiallyTrustedCallers屬性,不然沒法在非強命名的Assembly中訪問。
我們通過配置文件來修改引用的Assembly,可能通過應用程序配置文件,GAC的Publisher Policy File(什麼來的?)和機器的配置文件來更新,但通常情況下,不要去修改機器配置文件,對於單個應用程序的更新,通過應用程序配置文件來更新,而對於多個應用程序之間共用Assembly的更新,通過Publisher Policy File來更新。

(1) 創建CLS-Compliance的Assembly

CLS是Common Language Subsystem,遵循CLS的Assembly能夠被其它不同語言的程序所使用。順從CLS的Assembly必須限制Assembly的公共接口在CLS的規定裏面。具體來說,可以分成以下兩點:(a) Pulbic或者Protected的函數的方法的返回值,參數必須是在CLS規定裏面的;(b) 所有非CLS-Compliant的public或者Protected變量必須包含一個CLS-Compliant等價的方法。
在Assemply加上CLSCompliant(true)就能夠保證編譯器檢查出所有不遵循CLS規定的public或者Protected方法。比如:public UInt32 Foo(){}就不能通過編譯,因爲UInt32不是CLS-Compliant的。
對於Assembly是供其它程序使用的,如果這個Assembly不是CLS-Compliant的,則使用它的Assembly要想達到CLS-Compliant就會比較困難。
對於操作符重載,不是所有的語言都支持操作符重載的,CLS標準也沒有規定不能採用。對於重載了的操作符,在支持操作符重載的函數中使用時,就一樣可以使用,而在不支持操作符重載的函數中,則需要通過op_XXX函數來調用。比如op_equals就對應於重載的=號操作符。所以如果你希望你重載的操作符被其它語言所使用,最好提供一個等價的函數,這樣,別的語言在使用的時候就可以直接調用那個函數。
最後就要特別注意多態的參數通過接口暴露出非CLS-Compliant的類。最常見的就是事件的參數,比如:
Internal class BadEventArgs : EventArgs { internal UInt32 ErrorCode; }
public delete void MyEventHandler(object sender, EventArgs args);
public event MyEventHandler OnStuffHappers;
BadEventArgs arg = new BadEventArg();
OnStuffHappens(this, arg);
非CLS-Compliant的BadEventArgs通過事件傳遞給其它語言,但其它語言並不能處理這個參數,而引起錯誤。
對於接口來說,如果被聲明在CLS-Compliant的Assembly裏面,那麼他就是CLS-Compliant的,否則就不是,即使他的參數那些都順應了CLS的規定。
對於一個非CLS-Compliant的接口,你可以在CLS-Compliant的類中實現這個接口,但必須用的接口實現,例如:
public inter IFoo { void DoStuff (UInt32 arg); }
public class MyType : IFoo
{
    void IFoo.DoStuff(UInt32 arg) {}
}
IFoo並不是CLS-Compliant的,但在CLS-Compliant的MyType裏面,仍然可以實現這個接口,只不過對這個方法的引用不能通過MyType.DoStuff,而只能通過IFoo.DoStuff。這樣也沒有破壞MyType暴露出來的公共接口必須是CLS-Compliant的規則。因爲DoStuff是通過IFoo暴露出來的,本身IFoo就不是CLS-Compliant的。

(2) 儘量使用簡短的函數

一些有經驗的程序員經常會自己手工去優化代碼的效率,但實際上有時候會弄巧成拙。比如如果爲了避免函數的調用而寫一個很長很長的函數,反而會降低代碼執行的效率。
首先要知道編譯器首先生成中間語言存儲在Assembly裏面,然後由JIT編譯器將中間語言翻譯成機器代碼去執行。而JIT調用是由函數級別來調用的,只有調用到的函數纔會被翻譯成機器語言去執行。如果一個很長的函數裏面存在很複雜的邏輯,而且有些子句並不需要執行的,放在一個函數裏面會使得JIT必須將整個函數裏面的語句,包括不需要執行的語句,也翻譯成機器語言,然後執行。如果再把這個大的函數分成幾個小的函數,那麼只有使用到的函數纔會被JIT加載進來。特別是在if-else子句或者switch子句的時候,更是如此。
另一個原因是簡短的函數使得JIT的Enregistrations(把一些變量存儲在寄存器以加快運行的速度)變得更容易。簡短的使得JI更容易分辨哪些變量可以被放到寄存器裏面。
另外,JIT同時也決定了哪些函數要被內聯(Inline)。越是簡短的函數,越容易被內聯進執行代碼中。
在機器層次上的優化並不是程序員的責任,可以放心地把這些交給C#編譯器和JIT編譯器來做。他們所採用的算法並不是固定的,但肯定是最優或者比較優的,而且這些算法也隨着時間的發展而改進,我們不需要自己手工去優化這些效率。我們所要做的就是儘量使得函數簡單,短小。

(3) 創建小的,粘性高的Assembly

粘性(Cohesion) is the degree to which the responsibility of a single component form a meaningful unit.
相對於一個大的Assembly來說,使用多個小的Assembly可以使Assembly的更新更容易。更小的assembly也使得程序的啓動更快,雖然只有被調用的方法才被JIT轉換成機器代碼,但整個Assembly都會被加載進放在,並且CLR會爲每個方法都生成一個Stub。
當然,這也不是鼓勵你把一個類作爲一個Assembly,過多的assbmbly會降低性能。加載更多的assembly需要更多的工作;轉換成機器代碼也需要更多的時間,特別是解決函數的地址問題的時候,跨assembly需要更長的時間;跨assembly之間的安全檢查也會浪費時間,同一個assembly裏面的代碼是在同一個信任層次的,不同的assembly之間的訪問,CLR會檢查安全問題。
然後C#和.NET在設計的時候就支持很多的組件,因此更好的靈活性通常情況下也會值得這些代價,只要這些assembly之間的劃分是合理的。
另外,小也沒有一個絕對的概念,比如Microlib.dll就有24M,然而System.Web.RegularExpression就只有56K。

(4) 限制你的類的可見度

你應該給你的類最低的可見度(Visibility)。通常情況下,你都會高估了你的類所需的可見度。Private和Protected的類型可以實現public的接口,然後所有的用戶都能夠通過這個public的接口來訪問這個private的類型。類庫裏面比較經典的就是ArrayList裏面的ArrayListEnumerator,這個類實現了IEnumerator接口,但這個類是private的,所有的用戶都能夠訪問這個Private類的方法。
public class ArrayList : IEnumerable
{
    private class ArrayListEnumerator : IEnumerator {}
    public IEnumerator GetEnumerator()
    { return new ArrayListEnumerator(); }
}
通過降低你的類的可見度,你就降低了你以後更新類的時候需要修改的代碼。一旦你的類聲明爲public方法並且發佈之後,你就必須永遠維護這個接口,這樣就限制了以後你更新你代碼的靈活性。而對於private或者internal的方法,你可以更靈活地進行你所希望的所有修改。

(5) 創建高粒度的Web API

對於web程序來說,高粒度的API意味着客戶端和服務器端的傳輸次數變得更少。Web程序中最昂貴的操作在於服務器與客戶端的傳輸。客戶端應該一次性把所有服務器端需要的數據傳到服務器,服務器也應該一次性把客戶端所有需要的數據傳給客戶端。低粒度的API會使得客戶端和服務器端不斷地進行數據的交互。
當然,也要考慮到 Transaction的問題,而不是越多數據一次性傳輸越好,所以這裏面就存在着如何平衡的問題了。

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