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)。
 
無參屬性
許多類型都定義了能夠被獲取或者更改的狀態信息。這種狀態信息通常做爲類型的字段成員實現。
 
 
須要爭辯的是永遠都不該該像這樣來實現。面向對象的設計和編程的重要原則之一就是數據封裝(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編譯器在調試代碼時不會內聯屬性方法,由於內聯的代碼會變得難以調試。
相關文章
相關標籤/搜索