標籤:
執行個體構造器和類(參考型別)構造器(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, Language-integrated Query (LINQ))技術的基礎。C#編譯器允許建立一個委託,讓它引用一個對象上的擴充方法。 Action a = "Jeff".ShowItems;a();
分部方法 只能在部分類別或者結構中聲明分部方法的傳回型別始終是void,任何參數都不能用out修飾符來標記。因為,方法在運行時可能不存在,所以將一個變數初始化為方法也許會返回的東西。可以有ref參數,可以是泛型方法,可以是執行個體或者靜態方法。若是沒有對應的實現部分,便不能在代碼中建立一個委託來引用這個分部方法。分部方法總是被視為private方法。 ----------------------------------------------------------------------------------------------------
參數
選擇性參數和具名引數設計一個方法的參數時,可為部分或者全部參數分配預設值。
以傳引用的方式向方法傳遞參數預設情況下,CLR假定所有方法參數都是傳值的。傳遞參考型別的對象時,對一個對象的引用(或者說指向對象的一個指標)會傳給方法。注意這個引用(或者指標)本身是以傳值方式傳給方法的。這就意味著方法可以修改對象,而調用者可以看到這些修改。傳遞實值型別的執行個體時,傳給方法的是執行個體的一個副本,這意味著方法獲得它專用的一個實值型別執行個體的副本,調用者的執行個體並不受影響。
關鍵字out或refC#中,允許以傳引用而非傳值的方式傳遞參數。這是用關鍵字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, Language-integrated Query (LINQ))技術配合使用。可用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編譯器在調試代碼時不會內聯屬性方法,因為內聯的代碼會變得難以調試。
CLR via C#深解筆記四 - 方法、參數、屬性