實例構造與引用類型
之前的章節其實已經寫過了引用類型的構造過程:
首先當然是,在堆中,為引用類型的實例對象分配內存,然後初始化對象的附加字段(即類型對象指針和同步塊索引)。
這個時候為對象分配的內存都是直接被置為0的,所以如果所用到的構造器中沒有對對象中的一些字段做處理,那麼這些字段的初始值都應該為0或者null。
如果一個類,沒有構造函數,那麼這個類構造的時候就會定義一個默認無參構造器,它裡面就簡單調用基類的無參構造器。
極少數的情況下,會有不實用實例構造器就能創建類型的實例的情況,比如MemberwiseClone方法(深復制)和反序列化對象時。(反序列化會調用GetUninitializedObject或GetSafeUninitializedObject方法為對象分配內存,還是後面講吧。)
還記得上一章節寫的,內聯初始化可能會導致性能問題吧:
因為每一次內聯初始化實際上都會將這些初始化字段的操作,嵌入構造函數的代碼中(注意會先進行這些操作,再進行真正的構造函數的操作)。如果只有一個構造器函數,那麼不會有什麼影響。然而如果有多個構造器函數,那麼這幾個構造器函數裡都會插入這段初始化字段的代碼。
所以當存在多個構造器參數,而代碼裡又有一大堆內聯初始化,那麼實際生成的代碼中就會有大量的冗余代碼。可以用以下方法解決:
public class Troy{ //不進行內聯初始化 int _a; int _b; public Troy() { //將初始化過程放在某一構造參數內,一般就是無參構造函數中 this._a = 1; this._b = 2; } public Troy(int i):this() //在其它構造函數時,調用this() { } public Troy(int i,int j) : this() { } }
實例構造與值類型
值類型其實不需要定義構造器,C#編譯器也根本就不會為值類型內聯嵌入默認的無參構造器。
只有嵌套在引用類型的值類型才會被初始化為0或null,如果是基於棧的值類型,那麼在讀取之前,被要求強制初始化,否則報錯。
值類型的實例構造器只有顯式調用才有用,否則其字段都會被初始化為0或null。(也就是說,即時這個struct有無參構造函數,只要你沒有顯示調用,那麼就不會自動調用無參構造函數。實際上C#編譯器也不允許你在結構體裡寫一個無參構造函數,畢竟這個點來說,太容易誤會了)。
由於C#不允許值類型定義無參構造函數,所以值類型同樣不准內聯參數化。(靜態字段可以內聯初始化,因為是在類型對象裡面,而不是實例對象)
且值類型的任何構造函數在初始化的時候,必須對值類型的所有字段都賦值。
對於這麼麻煩的設定,當然也有解決的辦法:
public struct Troy { public int a; public int b; public Troy(int i) { this = new Troy();//先初始化所有的字段都為0或null //初始化自己想玩的字段 a = i; } }
(我不得不吐槽,我剛剛用VS自己寫了個值參數初始化的小例子,然後被360給當做病毒刪掉了。)
關於類型構造器
首先要了解到,類型構造器實際上就是構造CLR分配內存中的類型對象初始化時用的。
類型構造器是不允許有參數的,當然也就只能定義一個類型構造函數。
實際上類型構造器必須是私有的,甚至不允許顯式寫上private修飾符,這樣做正是為了防止開發人員調用。它的調用總是由CLR負責的。
簡單樣例:
class Program { static void Main(string[] args) { Troy obj = new Troy(); } } public class Troy { static Troy() { Console.WriteLine("我就問你6不6?"); } }
構造過程:
JIT編譯器編譯一個方法時,會查看代碼引用了哪些類型。任何一個類型定義了類型構造器,那麼JIT編譯器就會檢查針對當前AppDomain是否執行了這個類型構造器。是就不調用,否就調用。因為CLR希望在每個AppDomain中一個類型構造器只執行一次,所以為了不使多個線程同時調用類型構造器,在第一個調用類型構造器的線程調用時,會獲取一個互斥線程同步鎖。這樣一來,就只有一個線程可以調用了,後面的線程要用的時候發現已經調用過了,就不會再調用類型構造器了。(因為類型構造器線程安全,所以很適合在裡面初始化任何單例對象。)
雖然可以在值類型中定義類型構造函數,然而實際上因為值類型根本就不會在堆中有類型對象,所以自然裡面的代碼都不會被調用。
關於操作符重載
實際上CLR對操作符重載一無所知,因為這是編程語言的語法。
當C#這種語言寫的操作符重載語句被編譯成IL代碼時,其實已經變成了一個帶有specialname標志的函數。
當編譯器看到有+這種操作符時,就會看幾個操作數的類型中是否有定義了名為op_Addition這個函數(被編譯後的真正的函數名),而且該方法參數兼容於操作數的類型。
所以操作符重載函數中,一定要有一個參數的類型與定於這個重載方法的類型相同:
public class Troy { public static int operator +(Troy a, Troy b) { return 10; } }
關於轉換操作符方法
class Program { static void Main(string[] args) { Troy obj = 3;//隱式轉換成功 string a = obj;//由於是顯示轉換重載,所以這種寫法會編譯不過 string a =(String)obj;//顯示轉換成功 } } public class Troy { // 隱式轉換操作符implicit重載 public static implicit operator Troy(Int32 num) { return new Troy(); } // 顯式轉換操作符explicit重載 public static explicit operator String(Troy troy) { return "怎麼轉都是我"; } }
和一般的+-這種操作符重載一樣,實際上生成的IL代碼中,換了一個名字,前綴加上了op_。
當C#編譯器檢測到代碼中一個對象期望得到另一個類型不同的對象時,就回去找這兩個類型中是否定義了隱式轉換的op_Implicit方法,是就轉。顯示類似。
可以參考Decimal類的定義去理解。
關於擴展方法
先說一下我自己的認識吧,其實我不建議使用這個東西。
因為寫得不規范的擴展方法,會增加了代碼的閱讀難度,增加維護成本。(我真的很確定有的人會把這個東西寫得到處都是)
這個東西之前在寫《重構》的學習筆記中提到過,主要用於解決別人封裝的類庫,沒法增加自己想要的函數。
第19點.不完美的類庫講到的
簡單來講,還是慎用,自己寫的類就別用擴展方法。
另外擴展方法必須是頂級靜態類中定義的靜態方法,如果是嵌套類中的話,編譯會出錯。
實際上擴展方法在C#編譯器編譯過後也只是個一般的靜態對象裡的靜態函數,只不過加了個[Extension]的特性。然而實際上這個ExtensionAttribute特性還不能在代碼中用,都是C#編譯器去自動生成的。
關於分部方法
分部方法和分部類很像,不過是方法前面加上partial修飾符。
這樣的話,如果其它分部類實現了這個方法,那麼就會加上這個方法,如果沒有實現,那麼這條代碼在編譯的時候就會被忽略。
但是分部方法只能在分部類和結構中用,且返回類型總是void,任何參數都不能用out來修飾。之所以會這樣限制,是因為方法在運行時可能就並不存在,所以也就不會有返回。
分部方法總是private的,但是C#編譯器禁止將private修飾符顯式寫在分部方法前面。(和類型構造器在這個點上類似)