實例構造器和類(引用類型)
構造器(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)。
無參屬性
許多類型都定義了可以被獲取或者更改的狀態信息。這種狀態信息一般作為類型的字段成員實現。
需要爭辯的是永遠都不應該像這樣來實現。面向對象的設計和編程的重要原則之一就是數據封裝(data encapsulation)。它意味著類型的字段永遠不應該公開,因為這樣很容易寫出不恰當使用字段的代碼,從而破壞對象的狀態。
e.Age = -5; // 代碼被破壞
還有其他原因促使我們封裝對類型中的數據字段的訪問:
其一,你可能希望訪問字段來執行一些side effect、緩存某些值或者推遲創建一些內部對象。
其二,你可能希望以線程安全的方式訪問字段。
其三,字段可能是一個邏輯字段,它的值不由內存中的字節表示,而是通過某個算法來計算獲得。
基于上述原因,強烈建議將所有字段都設為private。要允許用戶或類型獲取或設置狀態信息,就公開一個針對該用途的方法。封裝了字段訪問的方法通常稱為訪問器(
accessor)方法。訪問器方法可選擇對數據的合理性進行檢查,確保對象的狀態永遠不被破壞。
這樣有兩個缺點。首先,因為不得不實現額外的方法,所以必須寫更多的代碼;其次,類型的用戶必須調用方法,而不能直接飲用一個字段名。
編程語言和CLR提供了一種稱為屬性(property)的機制。它緩解了第一個缺點所造成的影響,同時完全消除了第二個缺點。
可以將屬性想象成智能字段(smart field),即背后有額外邏輯的字段。CLR支持靜態、實例、抽象和虛屬性。另外,屬性可用任意”可訪問性“修飾符來標記,而且可以在接口中定義。
某個屬性都有一個名稱和一個類型(類型不能是void)。通過屬性的get和set方法操作類型內定義的私有字段,這種做法十分常見。私有字段通常稱為支持字段(backing field)。但是,get和set方法并不是一定要訪問支持字段。
C#內建了對屬性的支持,當C#編譯器發現代碼視圖獲取或者設置一個屬性時,它實際上會生成對上述某個方法的一個調用。除了生成對應的訪問器方法,針對源代碼中定義的每一個屬性,編譯器還會在托管程序集的元數據中生成一個屬性定義項。在這個記錄項中,包含了一些標志(flags)以及屬性的類型。另外,它還引用了get和set訪問器方法。這些信息唯一的作用就是在”屬性“這種抽象概念與它的訪問器方法之間建立起一個聯系。CLR并不使用這些元數據信息,在運行時只需要訪問器方法。
合理定義屬性
屬性看起來與字段相似,但本質上是方法。這一點引起了很多誤解。
對象和集合初始化器
常需要構造一個對象,然后設置對象的一些公共屬性(或字段)。下面的初始化方法簡化了對象初始化編程模式:
匿名類型
C#的匿名類型功能,可以使用非常簡潔的語法來聲明一個不可變的元組類型。元組類型是含有一組屬性的類型,這些屬性通常以某種形式相互關聯。
創建匿名類型,沒有在new關鍵字后執行類型名稱,編譯器會為其自動創建一個類型名稱,而且不會告訴我這個名稱具體是什么(這正是匿名一詞的來歷)。可以利用C#的”隱式類型局部變量“功能(var)。
編譯器定義匿名類型非常”善解人意“,如果它看到你在源代碼中定義了多個匿名類型,而且這些類型具有相同的結構,那么它只會創建一個匿名類型定義,但創建該類型的多個實例。所謂”相同結構“,是指在這些匿名類型中,每個屬性都有相同的類型和名稱,而且這些屬性的指定順序相同。正是類型的同一性,可以創建一個隱式類型的數組,在其中包含一組匿名類型的對象。
匿名類型經常與LINQ(Language Intergrated Query, 語言集成查詢)技術配合使用。可用LINQ執行查詢,從而生成由一組對象構成的集合,這些對象都是相同的匿名類型。然后,可以對結果集中的對象進行處理。所有這些都是在同一個方法中發生。匿名類型的實例不能泄露到一個方法的外部。方法原型中,無法要求它接受一個匿名類型的參數,因為沒有辦法執行匿名類型。也無法指定它返回對一個匿名類型的引用。
除了匿名類型和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編譯器在調試代碼時不會內聯屬性方法,因為內聯的代碼會變得難以調試。