1.實例構造器ctor(引用類型)
創建引用類型的實例時的步驟:
首先,為實例的數據字段分配內存;
接著,初始化對象的系統開銷字段(類型對象指針和同步塊索引);
最後,調用類型的實例構造器設置對象的初始狀態。
ctor不能被繼承,不能用virtual,new,override,sealed,abstract。
如果類中沒有顯示定義任何ctor,則默認定義一個無參ctor,這個ctor不執行任何語句,只是調用基 類的無參ctor;當然,如果類中有ctor,則不存在這個默認的無參ctor。
如果類為abstract的,則默認的ctor是protected;否則這個默認ctor都是public的。
如果類為static的,則不會生成默認的ctor。
如果基類A中沒有提供無參ctor,這裡,我們只考慮A中只有一個有參ctor(如果連這個ctor也沒有, 那麼A就是上面的那種情況了),這時,子類B必須顯示調用基類的ctor,否則不能編譯,如下所示:
public class A { public A(int t) { } } public class B : A { public B(int t) : base(t) //必須顯示調用 { } }
最終,都會調用到System.Object的公有無參ctor。
不需要實例構造器的時候:反序列化;Object.MemberwiseClone()方法。
內聯方法,其實就是在默認無參ctor中賦值。
2.實例構造器ctor(值類型)
1.C#中,struct不能包含顯示的無參ctor——CLR允許struct有無參ctor
2.struct一定要顯示調用其有參ctor,這樣在new新對象的時候,才會執行ctor中的語句
3.CLR會保證所有實例字段(值類型和引用類型)都被初始化為0或者null (如果沒有手動設置,就由CLR 自動分配)。
4.在struct中,不能使用內聯直接給字段賦值(初始化只能在顯示ctor中做)
5.每個字段都要在ctor中初始化,不然編譯器會報錯
3.靜態構造器cctor
cctor可以用於接口/引用類型/值類型,但是C#不支持接口。
cctor位於AppDomain中,在類第一次被訪問時執行。
cctor不能超過一個(可以沒有),而且永遠沒有參數。
cctor是私有的,但不能顯示聲明為私有。
在值類型中定義cctor是沒有意義的——沒有機會執行cctor。
cctor的調用過程:
編譯時,JIT編譯器將檢查AppDomain是否執行了cctor(有記錄的),以決定是否生成調用代碼;
執行時,如果有多個線程,則需要一個互斥的線程同步鎖,以確保只執行一次cctor。
避免在兩個類的cctor中互相引用,CLR不能確保先執行哪一個cctor,可以編譯,但是返回指不唯一。
如果cctor拋出異常,CLR會認為該類型不可用,與此類型相關的操作都會拋出 System.TypeInitializationException異常。
cctor中只能訪問靜態字段,它的用途就是初始化這些靜態字段。也可以使用內聯初始化,與cctor效 果一樣。
cctor不應調用其基類的cctor,兩者沒有關系。
CLR不支持靜態的Finalize()方法。
cctor的性能:
精確語義Precise:針對在cctor中初始化而言;
字段初始化前語義BeforeFieldInit:針對於內聯初始化而言。
二者的區別在於是否會在MSIL中的cctor上附加一個BeforeFieldInit標記。
測試代碼如下:
class Program { static void Main() { const Int32 iterations = 1000 * 1000 * 1000; PrefTest1(iterations); PrefTest2(iterations); Console.ReadLine(); } private static void PrefTest1(Int32 iterations) { Stopwatch sw = Stopwatch.StartNew(); for (Int32 i = 0; i < iterations; i++) { BeforeFieldInit.s_x = 1; } Console.WriteLine("PrefTest1: {0} BeforeFieldInit", sw.Elapsed); sw = Stopwatch.StartNew(); for (Int32 j = 0; j < iterations; j++) { Precise.s_x = 1; } Console.WriteLine("PrefTest1: {0} Precise", sw.Elapsed); } private static void PrefTest2(Int32 iterations) { Stopwatch sw = Stopwatch.StartNew(); for (Int32 i = 0; i < iterations; i++) { BeforeFieldInit.s_x = 1; } Console.WriteLine("PrefTest2: {0} BeforeFieldInit", sw.Elapsed); sw = Stopwatch.StartNew(); for (Int32 j = 0; j < iterations; j++) { Precise.s_x = 1; } Console.WriteLine("PrefTest2: {0} Precise", sw.Elapsed); } }
*Stopwatch類,提供一組方法和屬性,可以准確地測量運行時間
4.操作符重載
CLR不知道操作符是什麼;操作符是C#定義的,相應的生成編譯器識別的語言。
操作符重載是public static的,有一元重載和二元操作兩種方式。
要求重載方法的參數至少有一個參數與重載方法的類型一樣。
運算符參數不能使用ref/out修飾符。
例子:對於運算符+,
在定義時,會在編譯器中生成op_Addition()方法,在方法定義表中,這個方法屬於specialname組— —說明它是一個特殊方法
在調用時,編譯器會在specialname組查找相應的op_Addition()方法,如果存在而且方法參數匹配, 則執行;否則,編譯錯誤。
對於操作符重載,建議同時定義一個友好的方法,如重載+,同時定義一個Add方法。
操作符語法詳見http://www.cnblogs.com/Jax/archive/2007/09/13/891984.html
5.轉換操作符方法
System.Decimal是一個很好的學習操作符重載/轉換的Sample
為一個Rational定義轉換符構造器和方法:
操作符轉換也是public static的
public sealed class Rational { public Rational(Int32 num) { //由Int32構建一個Rational } public Rational(Single num) { //由Single構建一個Rational } public Int32 ToInt32() { //將Rational轉換為Int32 } public Single ToSingle() { //將Rational轉換為Single } //將Int32隱式轉為Rational,回調Rational(Int32)構造器 public static implicit operator Rational(Int32 num) { return new Rational(num); } //將Single隱式轉為Rational,回調Rational(Single)構造器 public static implicit operator Rational(Single num) { return new Rational(num); } //將Rational顯式轉為Int32,回調ToInt32() public static explicit operator Int32(Rational r) { return r.ToInt32(); } //將Rational顯式轉為Single,回調ToSingle() public static explicit operator Single(Rational r) { return r.ToSingle(); } }
由上面代碼可以看出,implicit/explicit是對兩個構造器和兩個ToXXX()方法的包裝。
相應的IL代碼:
public static Rational op_Implicit(Int32 num) public static Rational op_Implicit(Single num) public static Int32 op_Explicit(Rational r) public static Single op_Explicit(Rational r)
第3個和第四個方法僅僅是返回類型不同,這樣的語法只有在IL中允許。可以看到,操作符轉換實際上 是在利用IL的這一特性。
6.通過引用向方法傳遞參數
默認CLR的方法參數都是按值傳遞的。無論引用還是值類型參數,都是傳遞一個copy的副本——這樣意 味著方法可以修改對象,而不對方法外的對象有影響,僅在方法內部有影響。
CLR允許按照引用方式傳遞參數:out,ref,在聲明和調用時都要加上ref/out關鍵字
二者在CLR中等效,在C#中有區別:
不能將未初始化的參數作為ref傳遞到方法
//正確使用ref
static void Main() { int x = 2; GetRef(ref x); } private static void GetRef(ref int x) { x ++; }
//錯誤使用ref
static void Main() { int x; //未初始化,即使方法中賦值也不行 GetRef(ref x); } private static void GetRef(ref int x) { x = 10; }
在out方法中必須給out參數初始化賦值,這時,無論調用前是否給out參數初始化賦值,out方法中都 要賦值,否則會編譯錯誤。示例如下:
//正確使用out static void Main() { //以下兩句話都是對的 int x; int x = 10; GetOut(out x); } private static void GetOut(out int x) { x = 10; //一定要初始化,即使調用前已經初始化了 } //錯誤使用out static void Main() { int x = 10; GetOut(out x); } private static void GetOut(out int x) { x ++; //一定要初始化,即使調用前已經初始化了 }
方法可以基於ref/out可以重載,ref/out也算是方法簽名的一部分。但是重載不能僅限於ref和out的 差別,如下:
可以有
public sealed class Point { static void Add(Point p); static void Add(ref Point p); }
不能有
public sealed class Point { static void Add(Point p); static void Add(ref Point p); static void Add(out Point p); }
用ref實現交換兩個引用類型的方法:標准寫法,使用泛型
public static void Swap<T>(ref T a, ref T b) { T t = b; b = a; a = t; } public static void SomeMethod() { String s1 = "Jax.Bao"; String s2 = "Fish.Xu"; Swap(ref s1, ref s2); }
out/ref在值類型上使用和引用類型上使用行為相同。僅差在值類型實例分配到的是內存,引用類型分 配到的是指針。
以後的FileStream可以寫成以下模式:
static void Main() { FileStream fs = null; ProcessFiles(ref fs); for (; fs != null; ProcessFiles(ref fs)) { fs.Read(.); } } private static void ProcessFiles(ref FileStream fs) { if (fs != null) { fs.Close(); } if (noMoreFileToProcess) { fs = null; } else { fs = new FileStream(.); } }
7.向方法傳遞可變數量的參數 params關鍵字
只有最後一個參數可以是params的,而且必須是一個一維數組,可以傳遞null或0個元素的數組,甚至 是不傳值(忽略此參數,會默認生成0長的數組作為方法參數)。
調用的時候,不用創建數組對象,直接在方法中傳入任意數量的參數。
static void Main()
{
Console.WriteLine(TestParams());
Console.WriteLine(TestParams("1", "2"));
}
public static int TestParams(params string[] p1)
{
return p1.Length;
}
8.聲明方法的參數類型
原則1:方法參數盡可能指定為最弱的類型
選用IEnumberable<T>,而不是IList<T>或ICollection<T>
原則2:方法返回類型盡可能指定為最強的類型
返回FileStream,而不是Stream
原則3:如果希望在不影響調用代碼的情況下,能對方法內部實現進行修改,這時候返回類型要使用最 弱類型中的最強的一個。
使用IList<T> ,而不是List<T>
9.CLR不支持常量方法和常量參數