CLR via C#深解筆記四,
實例構造器和類(引用類型)
構造器(constructor)是允許將類型的實例初始化為良好狀態的一種特殊方法。構造器方法在“方法定義元數據表”中始終叫.ctor。
創建一個引用類型的實例時:
#1, 首先為實例的數據字段分配內存
#2, 然後初始化對象的附加字段(類型對象指針和同步塊索引)
#3, 最後調用類型的實例構造器來設置對象的初始狀態
構造引用類型的對象時,在調用類型的實例構造器之前,為對象分配的內存總是先被歸零。構造器沒有顯示重寫的所有字段保證都有一個0或null值。和其它方法不同,實例構造器永遠不能被繼承。
如果基類沒有提供無參構造器,那麼派生類必須顯式調用一個基類構造器,否則編譯器會報錯。如果類的修飾符為static(sealed和abstract - 靜態類在元數據中是抽象密封類),編譯器根本不會在類的定義中生成一個默認構造器。
注意:編譯器在調用基類的構造器前,會初始化任何使用了簡化語法的字段,以維持源代碼給人留下的“這些字段總是一個值”的印象。
實例構造器和結構(值類型)
值類型(struct)構造器的工作方式與引用類型(class)的構造器截然不同。CLR總是允許創建值類型的實例,是沒有辦法阻止值類型的實例化。
類型定義無參構造器的,但是CLR是允許的。為了增強應用程序的運行時性能,C#編譯器不會自動地生成這樣的代碼 (自動調用值類型的無參構造器,即使值類型提供了無參構造器)。
所以,值類型其實並不需要定義構造器。C#編譯器也根本不會為值類型生成默認的無參構造器。CLR確實允許為值類型定義構造器,並且必須顯式調用,即使是無參構造器 (這樣是為了增強應用程序性能)。實際上C#編譯器也是不允許值
類型構造器
CLR還支持類型構造器(Type constructor), 也稱為靜態構造器(static constructor)、類構造器(class constructor)或者類型初始化器(type initializer)。類型構造器可應用於接口(C#編譯器不允許)、引用類型和值類型。
#1, 實例構造器的作用是設置類型的實例的初始狀態。對應地,類型構造器的作用是設置類型的初始狀態。類型默認是沒有定義類型構造器的。若是定義,也只能定義一個,並且永遠沒有參數的。
#2, 類型構造器不允許出現訪問修飾符,事實上它總是私有的,C#編譯器會自動標記為private。之所以私有,是為了阻止任何由開發人員寫的代碼調用它,對它的調用總是由CLR負責的。
#3, 類型構造器的調用比較麻煩。JIT編譯器在編譯一個方法時,會查看代碼中都引用了那些類型。任何一個類型定義了類型構造器,JIT編譯器都會檢查 - 針對當前AppDomain, 是否已經執行了這個類型構造器。如果構造器從未執行,JIT編譯器就會在它生成的本地(native)代碼中添加對類型構造器的一個調用。如果類型構造器已經執行,JIT編譯器就不添加對它的調用,因為他知道類型已經初始化了。
#4, 當方法被JIT編譯器編譯完畢之後,線程開始執行它,最終會執行到調用類型構造器的代碼。多個線程可能同時執行相同的方法。CLR希望確保在每個AppDomain中,一個類型構造器只能執行一次。為了保證這一點,在調用類型構造器時,調用線程要獲取一個互斥線程同步鎖。這樣一來,如果多個線程視圖同時調用某個類型的靜態類型構造器,只有一個線程才可以獲得鎖,其他線程會被阻塞(blocked)。第一個線程會執行靜態構造器中的代碼。當第一個線程離開構造器後,正在等待的線程將被喚醒,然後發現構造器的代碼已經被執行過。
#5, 雖然能在值類型中定義一個類型構造器,但永遠都不要真的那麼做,因為CLR有時不會調用值類型的靜態類型構造器。
#6, CLR保證一個類型構造器在每個AppDomain中只執行一次,而且(這種執行)是線程安全的,所以非常適合在類型構造器中初始化類型需要的任何單實例(singleton)對象。
最後,如果類型構造器拋出一個未處理的異常, CLR會認為這個類型不可用。試圖訪問該類型的任何字段或方法,都將導致拋出一個System.TypeInitializationException 異常。類型構造器中的代碼只能訪問類型的靜態字段,並且它的常規用途就是初始這些字段。和實例字段一樣,C#提供了一個簡單的語法來初始化類型的靜態字段。
操作符重載方法
有些編程語言是允許一個類型定義操作符應該如何操作類型的實例。如,許多類型(System.String)都重載了相等(==)和不等(!=)操作符。CLR對操作符重載一無所知,它們甚至不知道什麼事操作符。是編程語言定義了每個操作符的含義,以及當這些特殊符號出現時,應該生成什麼樣的代碼。
public sealed class Complex {
public static Complex operator+(Complex c1, Complex c2) { ... }
}
操作符和編程語言的互操作性:如果一個類型定義了操作符重載方法,Microsoft還建議類型定義更友好的公共靜態方法,並在這種方法的內部調用操作符重載方法。FCL的System.Decimal類型很好地演示了如何重載操作符並按照Microsoft的知道原則定義友好的方法名。
轉換操作符方法
有時需要將對象從一個類型轉換成一個不同的類型。例如,有時不得不將Byte類型轉換成為Int32類型。其實,當源類型和目標類型都是編譯器的基元類型時,編譯器自己就知道如何生成轉換對象所需的代碼。
有些編程語言(如C#)就有提供轉換操作符的重載。轉換操作符是將對象從一個類型轉換成另一個類型的方法。可以使用特殊的語法來定義轉換操作符的方法。CLR規范要求轉換操作符重載方法必須是public和static方法。
除此之外,C#要求參數類型和返回類型二者必有其一與定義轉換方法的類型相同。
相同在C#中,implicit關鍵字告訴編譯器為了生成代碼來調用方法,不需要在源代碼中進行顯式轉型。相反,explicit關鍵字告訴編譯器只有在發現了顯式轉型時,才調用方法。
在implicit或explicit關鍵字之後,要指定operator關鍵字告訴編譯器該方法是一個轉換操作符。在operator之後,指定對象需要轉換成什麼類型。在圓括號之內,則指定要從什麼類型轉換。
擴展方法
應用擴展方法:
C#只支持擴展方法,不支持擴展屬性、擴展事件、擴展操作等
擴展方法(第一個參數前面有this的方法)必須在非泛型的靜態類中聲明,然而類名沒有限制,可以隨便什麼名字。當然,擴展方法至少要有一個參數,而且只有第一個參數能用this關鍵字標記。
C#編譯器查找靜態類中定義的擴展方法時,要求這些靜態類本身必須具有文件作用域。
擴展方法擴展類型時,同時也擴展了派生類型。所以,不應該將System.Object用作擴展方法的第一個參數,否則這個方法在所有表達式類型上都能調用,造成Visual Studio的“ 智能感知“ 窗口被填充太多的垃圾信息。
擴展方法有潛在的版本控制問題。
擴展方法,還可以為接口類型定義擴展方法。
擴展方法是微軟的LINQ(Language Integrated Query, 語言集成查詢)技術的基礎。
C#編譯器允許創建一個委托,讓它引用一個對象上的擴展方法。
Action a = "Jeff".ShowItems;
a();
分部方法
只能在分部類或者結構中聲明
分部方法的返回類型始終是void,任何參數都不能用out修飾符來標記。因為,方法在運行時可能不存在,所以將一個變量初始化為方法也許會返回的東西。可以有ref參數,可以是泛型方法,可以是實例或者靜態方法。
若是沒有對應的實現部分,便不能在代碼中創建一個委托來引用這個分部方法。
分部方法總是被視為private方法。
----------------------------------------------------------------------------------------------------
參數
可選參數和命名參數
設計一個方法的參數時,可為部分或者全部參數分配默認值。
以傳引用的方式向方法傳遞參數
默認情況下,CLR假定所有方法參數都是傳值的。
傳遞引用類型的對象時,對一個對象的引用(或者說指向對象的一個指針)會傳給方法。注意這個引用(或者指針)本身是以傳值方式傳給方法的。這就意味著方法可以修改對象,而調用者可以看到這些修改。
傳遞值類型的實例時,傳給方法的是實例的一個副本,這意味著方法獲得它專用的一個值類型實例的副本,調用者的實例並不受影響。
關鍵字out或ref
C#中,允許以傳引用而非傳值的方式傳遞參數。這是用關鍵字out或ref來做到的,告訴C#編譯器生成元數據來指明該參數是傳引用的。編譯器也將生成代碼來傳遞參數的地址,而不是傳遞參數本身。調用者必須為實例分配內存,被調用者則操縱該內存(中的內容)。
CLR角度來看,關鍵字out和ref完全一致。這就是說,無論用哪個關鍵字,都會生成相同的IL代碼。元數據也幾乎一致,只有一個bit除外,它用於記錄聲明方法時指定的是out還是ref。
C#編譯器是將這兩個關鍵字區別對待的,而且這個區別決定了由哪個方法負責初始化所引用的對象。如果方法的參數用out來標記,表明不指望調用者在調用方法之前初始化好對象,返回前必須向這個值寫入。如果是ref來標記,調用者就必須在調用該方法前初始化參數的值,被調用的方法可以讀取值以及/或者向值寫入。
綜上所述,從IL和CLR角度看,out和ref是同一碼事:都導致傳遞指向實例的一個指針。但從編譯器角度看,兩者有區別的,編譯器會按照不同的標准(要求)來驗證你寫的代碼是否正確。
重要提示:
如果兩個重載方法只有out和ref的區別,那麼是不合法的,因為兩個簽名的元數據表示是完全相同的。
對於以傳引用的方式傳給方法的變量(實參),它的類型必須與方法簽名中聲明的類型(形參) 相同。
參數和返回類型的知道原則
#1,聲明方法的參數類型時,應盡量指定最弱的類型,最好是接口而不是基類。
例如,如果要寫一個方法來處理一組數據項,最好是用接口(比如IEnumerable<T>來聲明方法的參數),而不要用強數據類型(如List<T>)或者更強的接口類型(如ICollection<T> 或 IList<T>).
// 好
public void AddItems<T>(IEnumerable<T> collection) { ... }
// 不好
public void AddItems<T>(List<T> collection) { ... }
如果需要是一個列表(而非僅僅是可枚舉的對象),就應該將參數類型聲明為IList<T>。但是,仍然要避免將參數類型聲明為List<T>。
這裡的例子討論的是集合,是用一個接口體系結構來設計的。如果要討論使用基類體系結構設計的類,概念同樣適用。如:
// 好
public void ProcessBytes(Stream someStream) { ... }
// 不好
public void ProcessBytes(FileStream fileStream) { ... }
第一個方法能處理任何一種流,包括FileStream、NetworkStream和MemoryStream等。
第二種方法則只能處理FileStream流,這限制了它的應用。
#2,一般將方法的返回類型聲明為最強的類型(以免受限於特定的類型)。例如,最好聲明方法返回一個FileStream對象,而不是返回一個Stream對象。
// 好
public FileStream OpenFile() { ... }
// 不好
public Stream OpenFile() { ... }
如果某個方法返回一個List<String> 對象,就可能想在未來的某個時候修改它的內部實現,以返回一個String[]。如果希望保持一定的靈活性,以便將來更改方法返回的東西,請選擇一個較弱的返回類型。
----------------------------------------------------------------------------------------------------
屬性
屬性允許源代碼用一個簡化的語法來調用一個方法。CLR支持兩種屬性:無參屬性(parameterless property), 簡稱為屬性。有參屬性(parameterful property),即索引器(indexer)。
無參屬性
許多類型都定義了可以被獲取或者更改的狀態信息。這種狀態信息一般作為類型的字段成員實現。
C#的匿名類型功能,可以使用非常簡潔的語法來聲明一個不可變的元組類型。元組類型是含有一組屬性的類型,這些屬性通常以某種形式相互關聯。
匿名類型的實例不能洩露到一個方法的外部。方法原型中,無法要求它接受一個匿名類型的參數,因為沒有辦法執行匿名類型。也無法指定它返回對一個匿名類型的引用。
除了匿名類型和Tuple類型,還以注意下System.Dynamic.ExpandoObject 類(System.Core.dll程序集中定義)。這個類和C#的dynamic類型配合使用,就可以用另一種方式將一系列屬性(鍵值對)組合到一起,這樣做的結果在編譯時不時類型安全的,但語法看起來不錯。
有參屬性
無參屬性因為get訪問器方法不接收參數,又與字段的訪問有些相似,所以這些屬性很容易理解。除此之外,編譯器還支持所謂的有參屬性(parameterful property),它的get訪問器方法接受一個或者多個參數,set 訪問器接受兩個或多個參數。不同的編碼語言以不同的形式公開有參屬性,稱呼也有所不同。C#語言把他們稱為索引器。Visual Basic稱為默認屬性。
C#使用數組風格的語言來公開有參屬性(索引器)。換句話說,可將索引看做C#開發人員重載" []"操作符的一種方式。
CLR本身並不區分無參屬性和有參屬性。對CLR來說,每個屬性都只是類型中定義的一對方法和一些元數據。如前所述,不同的編程語言要求用不同的語法來創建和使用有參屬性。將this[...] 作為表達一個索引器的語法,純粹是C#團隊自己的選擇。所以,C#也只是允許在對象的實例上定義索引器,而不提供定義靜態索引器屬性的語法,雖然CLR是支持靜態有參屬性的。
調用屬性訪問器方法時的性能
對於簡單get和set訪問器方法,JIT編譯器會將代碼內聯(inline)。這樣一來,使用屬性(而不是使用字段)就沒有性能上的損失。內聯是將一個方法(或者當前情況下的訪問器方法)的代碼直接編譯到調用它的方法中。這避免了在運行時發出調用所產生的開銷,代價是編譯好的方法的代碼會變得更大。注意,JIT編譯器在調試代碼時不會內聯屬性方法,因為內聯的代碼會變得難以調試。