世界上存在着男人和女人,如果沒有某種東西把男人和女人連接起來構成“男女關系”,那么這些男人將立如樹樁,仰天長嘆,女人們將飄如小舟,盪無歸處,整個世界毫無生機,自然離合。C#語言的類也是如此,有了字段和屬性這些基礎數據,必然要有一種東西讓它們存儲着某種聯系且相互作用,它就是方法。這一章將介紹類中的構造器、方法以及方法參數。
構造函數也稱為構造器,在創建類或結構的時候,CLR會都會調用類的構造函數,對於結構,CLR可能會隱式地調用默認構造函數。構造函數是一種特殊的函數,它不能被繼承,可用public 和private修飾,但不能被virtual、new、override、sealed和abstract修飾。構造函數又分為實例構造器和類構造器。
(1)實例構造函數
在使用new創建某個類的對象時,CLR會首先為實例的數據字段分配內存,接着初始化該對象的對象指針和同步索引塊,最后調用該類的實例構造函數來初始化對象的初始數據成員。如果某個類沒有定義構造函數,則編譯器會自動在IL中生成默認的無參數實例構造函數代碼,IL中的.ctor代表着實例構造器。如下定義了一個類:
public class Code_04 { private int age = 100; }
在IL中生成了一個無參返回值類型為void的無參的實例構造函數,如圖:
並且對字段數值的初始化也是構造函數中進行的。如下IL:
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // 代碼大小 16 (0x10) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.s 100 IL_0003: stfld int32 ConsoleApp.Example04.Code_04::age IL_0008: ldarg.0 IL_0009: call instance void [mscorlib]System.Object::.ctor() IL_000e: nop IL_000f: ret } // end of method Code_04::.ctor
細心的你可能會發現此構造函數被修飾符public作用着,如果此類是一個抽象類(使用修飾符abstract),則編譯器生成的默認構造函數的可訪問性將是protected,如此一來也驗證了一句傳說:抽象類不能被實例化,只能被繼承實現。通過上面的IL,我們還可以看到,在此構造函數內,自動調用了基類(System.Object)的構造函數。一個類型可以定義多個簽名不同的構造函數,這些構造函數當然可以使用不同的可訪問性。如果在構造函數內指明要調用基類的某個構造函數,則在IL中會生成對那個指明的基類構造函數的調用。如下兩個類的定義:
public class Code_04_02 { private int age = 10; public Code_04_02() { age = 20; } public Code_04_02(int age) { this.age = age; } } public class Code_04_03 : Code_04_02 { private string name; public Code_04_03() : base(30) { name = "張三"; } }
Code_04_03類繼承了Code_04_02類,並且類Code_04_03的構造函數使用了基類的構造函數。來看一下編譯器生成的IL:
(1) 構造函數public Code_04_02(int age)的IL:
.method public hidebysig specialname rtspecialname instance void .ctor(int32 age) cil managed { // 代碼大小 25 (0x19) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.s 10 IL_0003: stfld int32 ConsoleApp.Example04.Code_04_02::age IL_0008: ldarg.0 IL_0009: call instance void [mscorlib]System.Object::.ctor() IL_000e: nop IL_000f: nop IL_0010: ldarg.0 IL_0011: ldarg.1 IL_0012: stfld int32 ConsoleApp.Example04.Code_04_02::age IL_0017: nop IL_0018: ret } // end of method Code_04_02::.ctor
這個構造函數內先對本類的數據字段進行初始化:IL_0001和IL_0003,接着調用基類(System.Object)的構造函數:IL_0009,然后是執行本構造函數內的代碼:IL_0010- IL_0012。
再來看一下構造函數public Code_04_03()的IL:
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // 代碼大小 23 (0x17) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.s 30 IL_0003: call instance void ConsoleApp.Example04.Code_04_02::.ctor(int32) IL_0008: nop IL_0009: nop IL_000a: ldarg.0 IL_000b: ldstr bytearray (20 5F 09 4E ) // _.N IL_0010: stfld string ConsoleApp.Example04.Code_04_03::name IL_0015: nop IL_0016: ret } // end of method Code_04_03::.ctor
先是調用基類的有參構造函數如果是一個靜態類(對類使用static修飾符),則編譯器不會自動生成默認的構造函數,下面會講到類構造函數:IL_0003,接着是初始化本類的數據字段。由於這里調用了基類的構造函數public Code_04_02(int age)(IL: IL_0003),而基類的構造函數Code_04_02(int age)調用它自己了基類(System.Object)的無參構造函數,所以這里少了一個對System.Object類的構造函數的調用。
通過以上分析,我們可以得出,實例構造函數總是會調用基類的構造函數,執行順序:
初始化本類的數據字段->調用基類的構造函數(有參或無參)->執行本構造函數內的邏輯。
值類型(結構struct)也可以實例化,但編譯器不會為值類型生成默認的構造函數。如下定義了一個結構:
public struct Code_04_04 { public string Address; }
再來看一下編譯器做的工作:
從圖中可以看出,編譯器並沒有為我們自動生成默認的構造函數。我們繼續對上面的代碼進行改造:
public struct Code_04_04 { public string Address; public Code_04_04(string address) { Address = "中國"; } }
現在再來看看編譯器,它生成了我們定義的有參數構造函數,IL:
.method public hidebysig specialname rtspecialname instance void .ctor(string address) cil managed { // 代碼大小 13 (0xd) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldstr bytearray (2D 4E FD 56 ) // -N.V IL_0007: stfld string ConsoleApp.Example04.Code_04_04::Address IL_000c: ret } // end of method Code_04_04::.ctor
仔細一看,還會發現值類型的構造函數內根本不再調用基類的構造函數,盡管它最終是繼承於System.Object。
然而,如果類使用static修飾符,則情況就大不相同了。
(2)類構造函數
類型構造函數 也稱為靜態構造函數或類型初始化器,它主要用於初始靜態數據成員。你可以明確定義一個靜態構造函數,也可以讓C#編譯器自動生成(在類使用static修飾符時),但無論如何,一個類型只能有一個靜態構造函數,並且不可有參數。以下的3種類定義,會生成功能相同的IL:
public static class Code_04_05 { static int age = 10; } public static class Code_04_06 { static int age = 10; static Code_04_06() { } } public class Code_04_07 { static int age = 10; string name = "張三"; }
來看看C#編譯器所做的工作:
靜態類Code_04_05 自動生成了靜態構造函數,靜態類Code_04_06 根據我們的定義生成了靜態構造函數,類Code_04_07 雖然不是靜態類,但由於它有靜態字段,所以C#編譯器也自動生成的靜態構造函數,目的是為了初始化靜態字段,同時也生成了實例構造函數,目的是為了初始化數據字段name。還有一點,你一定能發現,靜態構造函數的標記是.cctor。在這些靜態構造函數內執行大致相同的工作,那就是初始化靜態數據成員。下面是Code_04_05 類的靜態構造函數的IL:
.method private hidebysig specialname rtspecialname static void .cctor() cil managed { // 代碼大小 8 (0x8) .maxstack 8 IL_0000: ldc.i4.s 10 IL_0002: stsfld int32 ConsoleApp.Example04.Code_04_05::age IL_0007: ret } // end of method Code_04_05::.cctor
靜態構造函數都是默認的private,並且也是必須的,也不能明文為其使用訪問修飾符。
CLR能保證在訪問某類型的靜態數據成員之前調用該類的靜態構造函數。在調用 靜態構造函數時,JIT會檢查當前應用程序域是否已經執行過該靜態構造函數,如果未執行,則當前線程會獲取一個互斥線程同步鎖,其他線程被阻塞,當前線程會執行該靜態構造函數內的代碼,執行完畢后釋放同步鎖,由於靜態構造函數已經被執行過,所以其他線程可直接使用該類的靜態成員,如此來保存靜態構造函數在整個應用程序的生命周期內只執行一次。由於加鎖,所以調用靜態成員在當前應用程序域的整個應用程序的整個生命周期內都是線程安全的。
須要說明一點的是,如果是同一個線程內有兩個靜態構造函數包含了相互引用的代碼,有可能會發生資源競爭。詳細內容可查找相關資料。
方法是包含一系列語句的代碼塊,也稱為函數,上面所講的構造器就是一種特殊的方法。方法把程序代碼划分為多個聯連續但可能相互聯系的邏輯單元,如此一來,不僅可以代碼重用,也增強可讀性和方便調用。每個方法都必須有一個名稱和一個主體,在方法主體內進行邏輯處理,並且可以在方法內聲明臨時變量。
方法可以擁有(也可不必擁有)參數列表,方法參數括在括號中,並用逗號隔開。
方法可有也可心沒有返回值,當不需要返回數據時,我們通常將它的返回類型定義為void ,void是一個結構體類型,通常,我們稱“返回類型為void的方法為返回值為空的方法”,在方法體的最后可以用關鍵字return返回空值,也可以不用。如果定義了返回類型為非void 的方法,則必須使用return 返回一個與返回類型對應的值。
當然也可以為方法定義訪問級別,如:public 或 private,可選修飾符(例如 abstract 或 sealed),抽象方法和虛方法是兩類很特別的方法,我們會在以后的章節中詳細描述。
方法分為對象級方法和類級方法。很明顯,對象級方法是通過對象來訪問的,類級方法是通過類來訪問的,就像類的字段和屬性一樣。如下代碼定義了兩個方法:
public class Code_04_08 { string prefix = "_"; public void SetPrefix(string prefix) { this.prefix = prefix; //return; } public string GetName(string name) { return prefix + name; } public static int Add(int a, int b) { return a + b; } }
對方法的調用與調用類的字段、屬性很像。對象級方法的調用:
Code_04_08 temp = new Code_04_08(); string name = temp.GetName("張三");
類級方法的調用:
int a = Code_04_08.Add(1, 2);
C#還支持擴展方法和分部方法,詳細內容可參考MSDN: 擴展方法 和 分部類和方法。
構造一個方法時,可以為方法指定參數列表,如果沒有參數,方法的小括弧內為空即可,如:
public void Test() { }
可以使用值傳遞和引用傳遞兩種方式進行傳參,C#默認方法是值傳遞。
值傳遞 傳給方法的是值類型實例的一個副本。如下代碼:
public int Test(int c, List<string> names) { return c + names.Count; }
對此Test方法的調用:
int k = 10; List<string> names = new List<string>() { "張三" }; int m = Code_04_08.Test(k, names);
以上的調用代碼Code_04_08.Test(k, names);就是將k的值的副本傳遞給了方法Add。由於names是引用對象,所以傳遞給方法的是這個引用本身(指針)。在方法Add內直接拿到了names的指針,所以在方法體內是可以對names的原引用進行操作的,而不僅僅是讀取。
引用傳遞 是傳遞對象的引用而並非值的副本,它傳遞的是參數的地址,這樣一來,在方法體內得到了原有地址的引用,就可以對原有對象進行各種操作了。CLR使用out和 ref關鍵字實現對參數的引用傳遞。兩者最根本的區別是在哪里對參數進行初始化。
Out輸出參數 調用者可以對參數進行寫,且必須的由調用者在返回前對參數進行寫入值。
ref 參數 調用者可以對參數進行讀寫,調用者必須在調用該方法前對參數進行初始化,當然在被調方法內部是可以對其進行讀、寫的。如下方法定義:
public static string GetValue(out string name, ref string address) { name = "張三"; address = "中國"; return string.Format("{0}-{1}", name, address); } //對方法GetValue的調用: string myName; string myAddress = "China"; string stringValue = Code_04_08.GetValue(out myName, ref myAddress);
可以看到,在GetValue方法內部對name值進行了寫入,同時改寫了參數myAddress的值。由於是以引用方式傳遞參數,所以在方法GetValue內部參數的操作參直接反應到變量myName和myAddress 對應的值中。我們可以通過以下代碼來驗證:
Console.WriteLine(myName);
Console.WriteLine(myAddress);
最終打印的結果是:
以上的討論都是針對指定參數個數的方法。你可能會想問,那能不能向方法傳遞不定個數的參數呢?當然可以!C#使用關鍵字params允許你這樣做。如下代碼定義:
public static string GetStringByCount(int count, params string[] stringValue) { StringBuilder sb = new StringBuilder(); sb.Append(count); sb.Append(": "); for (int i = 0; i < stringValue.Length; i++) { sb.Append(stringValue[i]); sb.Append("|"); } return sb.ToString(); } //方法的定義可以接收不定個數字符串對此方法調用: string str = Code_04_08.GetStringByCount(10, "a001"); string str = Code_04_08.GetStringByCount(10, "a001", "b002"); string str = Code_04_08.GetStringByCount(10, "a001", "b002", "c003"); string str = Code_04_08.GetStringByCount(10, "a001", "b002", "c003", "d004");
非常幸運的是,還可以向這些不定個數的參數傳遞不定的類型,很顯然,能接收不定類型的實例對象的類型是System.Object,因為它是所有類型的最終基類。如下代碼:
public static void Show(params object[] objects) { foreach (object obj in objects) { Console.WriteLine(obj.ToString()); } } //可以向其傳遞任何類型的實例: Code_04_08.Show("abc", 200, 's', 3.14);