8.1 實例構造器和類(引用類型) 構造引用類型的對象時,在調用類型的實例構造器之前,為對象分配的記憶體總是先被歸零 。沒有被構造器顯式重寫的所有欄位都保證獲得 0 或 null 值。 構造器不能被繼承。不能使用以下修飾符: virtual,new,override,sealed和abstract. ...
8.1 實例構造器和類(引用類型)
構造引用類型的對象時,在調用類型的實例構造器之前,為對象分配的記憶體總是先被歸零 。沒有被構造器顯式重寫的所有欄位都保證獲得 0 或 null 值。
構造器不能被繼承。不能使用以下修飾符: virtual,new,override,sealed和abstract.
如果基類沒有提供無參構造器,那麼派生類必須顯式調用一個基類構造器,否則編譯器會報錯。
如果類的修飾符為 static(sealed 和 abstract),編譯器不會生成預設構造器。(靜態類在元數據中是抽象密封類)
為了使代碼“可驗證”,類的實例構造器在訪問從基類繼承的任何欄位之前,必須先調用基類的構造器。
如果派生類的構造器沒有顯式調用一個基類構造器,C#編譯器會自動生成對預設的基類構造器的調用。
極少數時候可以在不調用實例構造器的前提下創建類型的實例(如 MemberwiseClone 方法)。
不要在構造器中調用虛方法。 原因是假如被實例化的類型重寫了虛方法,就會執行派生類型對虛方法的實現。但在這個時候,尚未完成對繼承層次結構中的所有欄位的初始化(被實例化的類型的構造器還沒有運行呢)。所以,調用虛方法會導致無法預測的行為。
構造器中,先執行以“內聯”方式對欄位的初始化(C#編譯器將這種語法轉換成構造器方法中的代碼來執行初始化),再調用基類的構造器,最後執行當前構造器中的代碼。詳見下麵代碼:
public class AType { public AType() { Console.WriteLine(nameof(AType)); } } public class Ball { public Ball() { Console.WriteLine(nameof(Ball)); } } public class Basketball:Ball { //內聯語法,最終會把atype的初始化放在構造函數中 AType atype = new AType(); public Basketball() { Console.WriteLine(nameof(Basketball)); } } public class Program { static void Main() { Basketball ball = new Basketball(); Console.ReadLine(); } }
輸出結果為: AType,Ball,Basketball
然後又做了下麵的BT測試:
public class AType { public AType(string x) { Console.WriteLine(nameof(AType) + ":" + x); } } public class Ball { //在基類中新增加個內聯方式初始化的欄位 private AType atype = new AType(nameof(Ball)); public Ball() { Console.WriteLine(nameof(Ball)); } } public class Basketball : Ball { //內聯語法,最終會把atype的初始化放在構造函數中 private AType atype = new AType(nameof(Basketball)); public Basketball() { Console.WriteLine(nameof(Basketball)); } } public class Program { static void Main() { Basketball ball = new Basketball(); Console.ReadLine(); } }
輸出結果為:AType:Basketball, AType:Ball, Ball, Basketball
使用“內聯”方式初始化欄位,要註意代碼的膨脹效應。如果類中有多個構造器,編譯器會把“內聯”初始化代碼插到每一個構造器中,再插入基類構造器的調用,最後執行自己的代碼。
基於上面一點,可考慮不在定義欄位時初始化,而是創建單個構造器來執行這些公共的初始化。然後,讓其他構造器利用 this 顯式調用這個公共初始化構造器,如下麵代碼:
internal sealed class SomeType { //不要顯式初始化下麵的欄位 private int m_x; private string m_s; private double m_d; private byte m_b; //該構造器將所有欄位都設為預設值, //其他所有構造器都顯式調用該構造器 public SomeType() { m_x = 5; m_s = "Hi there"; m_d = 3.14159; m_b = 0xff; } //該構造器將所有的欄位都設為預設值,然後修改m_x public SomeType(int x) : this() { m_x = x; } //該構造器將所有的欄位都設為預設值,然後修改m_s public SomeType(string s) : this() { m_s = s; } //該構造器將所有的欄位都設為預設值,然後修改m_x和m_s public SomeType(int x, string s) : this() { m_x = x; m_s = s; } }
8.2 實例構造器和結構(值類型)
C#編譯器根本不會為值類型內聯(嵌入)預設的無參構造器。結構也不能包含顯式的無參數構造函數(編譯器會報錯)。
考慮到性能,CLR不會為包含在引用類型中的每個值類型欄位都主動調用構造器。值類型的實例構造器只有顯式調用才會執行。
值類型的任何構造器都必須初始化值類型的全部欄位。(如未全部初始化,編譯器會報錯)
8.3 類型構造器
類型構造器(靜態構造器) 可應用於介面(C#編譯器不允許)、引用類型和值類型(但請永遠不要在值類型中定義類型構造器,因為CLR有時不會調用值類型的靜態類型構造器)。
類型構造器必須是 private 的,但不能顯示標記,只能由編譯器來標記為 private .
類型構造器必須無參。
CLR保證一個類型構造器在每個AppDomain中只執行一次,而且(這種執行)是線程安全的。 所以非常適合在類型構造器中初始化類型需要的任何單實例(Singleton)對象。
8.4 操作符重載方法
CLR規範要求操作符重載方法必須是 public 和 static 方法。
C#(以及其他許多語言)要求操作符重載方法至少有一個參數的類型與當前定義這個方法的類型相同。
public sealed class Complex
{
//重載“+”操作符
public static Complex operator +(Complex c1, Complex c2){...}
}
- 使用不支持操作符重載的編程語言時,語言應該允許你直接調用希望的 op_* 方法(例如 op_Addition)。反之,如果在C#中引用了不支持操作符重載的語言所寫的類型,且類型中提供了一個 op_Addition 方法,這時依然不能使用+操作符來調用這個 op_Addition 方法,因為元數據中沒有關聯 specialname 標記。
8.5 轉換操作符方法
CLR要求操作符重載方法必須是 public 和 static 方法。
C#(以及其他許多語言)要求參數類型和返回類型二者必有其一與定義轉換方法的類型相同。
類型轉換模板代碼:
public sealed class Rational { //由一個Int32構造一個Rational public Rational(Int32 num) { ...} //由一個Single構造一個Rational public Rational(Single num) { ...} //將一個Rational轉換成一個Int32 public Int32 ToInt32() { ...} //將一個Rational轉換成一個Single public Single ToSingle() { ...} //由一個Int32隱式構造並返回一個Rational public static implicit operator Rational(Int32 num) { return new Rational(num); } //由一個Single隱式構造並返回一個Rational public static implicit operator Rational(Single num) { return new Rational(num); } //由一個Rational顯示返回一個Int32 public static explicit operator Int32(Rational r) { return r.ToInt32(); } //由一個Rational顯示返回一個Single public static explicit operator Single(Rational r) { return r.ToSingle(); } } //像前面那樣為 Rational 類型定義了轉換操作符之後,就可以寫出像下麵這樣的C#代碼: public sealed class Program { public static void Main() { Rational r1 = 5; //Int32 隱式轉型為 Rational Rational r2 = 2.5F; //Single 隱式轉型為Rational Int32 x = (Int32)r1; //Rational 顯式轉型為Int32 Single s = (Single)r2; //Rational 顯式轉型為Single } }
implicit 關鍵字告訴編譯器可以隱式轉換; explicit 表示必須顯式轉換。
在 implicit 或 explicit 關鍵字之後,要指定 operator 關鍵字告訴編譯器該方法是一個轉換操作符。
在 operator 之後,指定對象要轉換成什麼為型。在圓括弧內,則指定要從什麼類型轉換。
不損失精度時用 implicit ,否則用 explicit .顯式轉換失敗,應該拋出 OverflowException 或者 InvalidOperationException 異常。
8.6 擴展方法
翠花,上代碼:
//靜態的類
public static class StringBuilderExtensions {
//靜態方法,this 關鍵字
public static Int32 IndexOf(this StringBuilder sb, Char value) {
for (Int32 index = 0; index < sb.Length; index++)
if (sb[index] == value) return index;
return -1;
}
}
現在,就可以像這樣使用Int32 index=sb.IndexOf('!')
8.6.1 規則和原則
C#只支持擴展方法,不支持擴展屬性、擴展事件、擴展操作符等。
擴展方法(第一個參數前面有 this 的方法)必須在非泛型的靜態類中聲明。類名沒有限制。至少要有一個參數,而且只有第一個參數能用 this 關鍵字標記。
如果有人在 Wintellect 命名空間中定義了一個 StringBuilderExtensions 類,那麼程式員為了訪問這個類的擴展方法,必須在他的源代碼文件頂部寫一條
using Wintellect;
指令。擴展方法可能存在版本控制問題。如果 Microsoft 未來為他們的 StringBuilder 類添加了 IndexOf 實例方法,而且和我的代碼調用的原型一樣,那麼在重新編譯我的代碼時,編譯器會綁定到Microsoft的IndexOf實例方法,而不是我的靜態IndexOf方法。這樣我的程式就會有不同的行為。
8.6.2 用擴展方法擴展各種類型
為介面定義擴展方法
public static void ShowItems<T>(this IEnumerable<T> collection) { foreach (var item in collection) Console.WriteLine(item); } public static void Main() { //每個Char在控制臺上單獨顯示一行 "Grant".ShowItems(); //每個String在控制臺上單獨顯示一行 new[] { "Jeff", "Kristin" }.ShowItems(); //每個Int32在控制臺上單獨顯示一行 new List<Int32>() { 1, 2, 3 }.ShowItems(); }
為委托類型定義擴展方法
public static void InvokeAndCatch<TException>(this Action<object> d, object o) where TException : Exception { try { d(o); } catch (TException) { } } public static void Main() { //創建一個Action委托(實例)來引用靜態 ShowItems 擴展方法。 //並初始化第一個實參來引用字元串“Jeff” Action a = "Jeff".ShowItems; //調用(Invoke)委托,後者調用(call)ShowItems. //並向它傳遞對字元串“Jeff”的引用 a(); }
8.6.3 ExtensionAttribute類
8.7 分部方法
//工具生成的代碼,存儲在某個源代碼文件中
internal sealed partial class Base
{
private String m_name;
//這是分部方法的聲明
partial void OnNameChanging(String value);
public String Name
{
get { return m_name; }
set
{
OnNameChanging(value.ToUpper()); //通知類要進行更改了
m_name = value; //更改欄位
}
}
}
//開發人員生成的代碼,存儲在另一個源代碼文件中:
internal sealed partial class Base
{
//這是分部方法的實現,會在m_name更改前調用
partial void OnNameChanging(string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException("value");
}
}
規則和原則:
它們只能在分部類或結構中聲明。
分部方法的返回類型始終是 void ,任何參數都不能用 out 修飾符來標記,之所以如此要求,都是因為——方法可能不存在。
可以有 ref 參數,可以是泛型方法,可以是實例或靜態方法,而且可標記為 unsafe 。
分部方法的聲明和實現必須具有完全一致的簽名。
如果沒有對應的實現部分,便不能在代碼中創建一個委托來引用這個分部方法(編譯器會報錯)。同樣是因為——方法可能不存在。
分部方法總是被視為 private 方法,但C#編譯器禁止在分部方法聲明之前添加 private 關鍵字。