構造器
構造器(構造函數)是將類型的實例初始化的特殊方法。構造器可分為實例構造器和類型構造器,本節將詳細介紹有關內容。
實例構造器
顧名思義,實例構造器的作用就是對類型的實例進行初始化。如果類沒有顯示定義任何構造器,C#編譯器會定義一個默認的無參構造器。相反,如果類中已經顯示地定義了一個構造器,那么就不會再生成默認構造器了。定義實例構造器的語法這里就不再多做闡述了(該懂得要懂呀),下面通過一個簡單的示例講述實例構造器的執行原理。
public class Rapper
{private string name;private int age;private bool real = true;public Rapper(string name,int age){this.name = name;this.age = age;}
}
通過上述代碼,我們創建了一個Rapper類,并定義了一個實例構造器,下面通過ildasm.exe工具查看構造器方法(.ctor)的IL代碼。
.method public hidebysig specialname rtspecialname instance void .ctor(string name,int32 age) cil managed
{// Code size 30 (0x1e).maxstack 8IL_0000: ldarg.0IL_0001: ldc.i4.1IL_0002: stfld bool ConsoleApplication5.Rapper::realIL_0007: ldarg.0IL_0008: call instance void [mscorlib]System.Object::.ctor()IL_000d: nopIL_000e: nopIL_000f: ldarg.0IL_0010: ldarg.1IL_0011: stfld string ConsoleApplication5.Rapper::nameIL_0016: ldarg.0IL_0017: ldarg.2IL_0018: stfld int32 ConsoleApplication5.Rapper::ageIL_001d: ret
} // end of method Rapper::.ctor
執行步驟:
- Rapper的構造器把值true存儲到字段real
- 調用Object類的構造器
- 加載第一個參數存儲到字段name
- 加載第二個參數存儲到字段age
雖然我們在聲明real字段時直接賦值為true,但是在編譯時,編譯器會將這種語法轉換成構造器方法中的代碼來進行初始化。
我們知道,一個類型可以定義多個構造器,每個構造器須有不同簽名,將Rapper類稍加修改.
public class Rapper
{private string name;private int age;private bool real = true;private bool diss = true;private bool forgetWords = true;public Rapper(string name, int age){this.name = name;this.age = age;}public Rapper(string name){this.name = name;}
}
通過ildasm.exe工具查看兩段構造器的IL代碼,會發現在每個方法開始的位置都包含用于初始化real,diss,forgetWords的代碼。
為了減少生成的代碼,可以利用this關鍵字顯式調用另外一個構造器
public class Rapper
{private string name;private int age;private bool real = true;private bool diss = true;private bool forgetWords = true;public Rapper(string name, int age) : this(name){this.age = age;}public Rapper(string name){this.name = name;}
}
到目前為止,我們討論的都是引用類型的實例構造器,下面,再來看看值類型的實例構造器。這里只用一句話來概括:值類型不允許包含顯式的無參數構造器,如果為值類型定義構造器,必須顯示調用才會執行。
類型構造器
類型構造器也稱靜態構造函數,類型構造器的作用是設置類型的初始狀態。類型默認沒有定義類型構造器。如果定義,只能定義一個。類型構造器沒有參數。
類型構造器的特點:
- 定義類型構造器類似于實例構造器,區別在于必須標記為static
- 類型構造器總是私有的,靜態構造器不允許出現訪問修飾符
類型構造器的執行原理:
- JIT編譯在編譯一個方法時,會查看代碼中所有引用的類型
- 判斷類型是否定義了類型構造器
- 針對當前的AppDomain,檢查是否已經調用了該類型構造器
- 如果沒有,JIT編譯器會在生成的native代碼中添加對類型構造器的調用
類型構造器中的代碼只能訪問靜態字段,與實例構造器相同,在類中聲明靜態字段并直接賦值時,編譯器會自動生成一個類型構造器,并在類型構造器中初始化該值。為上面的Rapper類添加靜態字段hobby
private static string hobby = "rap";
查看類型構造器方法(.cctor)的IL代碼。
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
{// Code size 11 (0xb).maxstack 8IL_0000: ldstr "rap"IL_0005: stsfld string ConsoleApplication5.Rapper::hobbyIL_000a: ret
} // end of method Rapper::.cctor
操作符重載方法
有的語言允許類型定義操作符來操作類型的實例。CLR對操作符一無所知,是編程語言定義了每個操作符的含義,以及調用這些操作符時生成的代碼。向Rapper類添加如下代碼:
public static string operator +(Rapper rapperA, Rapper rapperB){if (rapperA.name == "PGOne" || rapperB.name == "PGOne"){return "diss";}return "peace";}
注意:
- CLR規范要求操作符重載方法必須是public和static方法
- 使用operator關鍵字告訴編譯器,這是一個自定義操作符重載方法
修改Main方法,聲明兩個Rapper對象,并輸出rapperA + rapperB的返回值。
class Program
{static void Main(string[] args){Rapper rapperA = new Rapper("PGOne");Rapper rapperB = new Rapper("GAI");Console.WriteLine(rapperA + rapperB); //dissConsole.ReadLine();}
}
下面,使用ILDasm.exe工具查看編譯器生成的IL代碼。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 43 (0x2b)
.maxstack 2
.locals init ([0] class ConsoleApplication5.Rapper rapperA,[1] class ConsoleApplication5.Rapper rapperB)
IL_0000: nop
IL_0001: ldstr "PGOne"
IL_0006: newobj instance void ConsoleApplication5.Rapper::.ctor(string)
IL_000b: stloc.0
IL_000c: ldstr "GAI"
IL_0011: newobj instance void ConsoleApplication5.Rapper::.ctor(string)
IL_0016: stloc.1
IL_0017: ldloc.0
IL_0018: ldloc.1
IL_0019: call string ConsoleApplication5.Rapper::op_Addition(class ConsoleApplication5.Rapper,class ConsoleApplication5.Rapper)
IL_001e: call void [mscorlib]System.Console::WriteLine(string)
IL_0023: nop
IL_0024: call string [mscorlib]System.Console::ReadLine()
IL_0029: pop
IL_002a: ret
} // end of method Program::Main
通過IL_0019一行,我們可以看到代碼中出現+操作符時,實際調用的是op_Addition方法,再查看op_Addition方法的IL代碼。
.method public hidebysig specialname static string op_Addition(class ConsoleApplication5.Rapper rapperA,class ConsoleApplication5.Rapper rapperB) cil managed
{
// Code size 61 (0x3d)
.maxstack 2
.locals init ([0] bool V_0,[1] string V_1)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld string ConsoleApplication5.Rapper::name
IL_0007: ldstr "PGOne"
IL_000c: call bool [mscorlib]System.String::op_Equality(string,string)
IL_0011: brtrue.s IL_0025
IL_0013: ldarg.1
IL_0014: ldfld string ConsoleApplication5.Rapper::name
IL_0019: ldstr "PGOne"
IL_001e: call bool [mscorlib]System.String::op_Equality(string,string)
IL_0023: br.s IL_0026
IL_0025: ldc.i4.1
IL_0026: stloc.0
IL_0027: ldloc.0
IL_0028: brfalse.s IL_0033
IL_002a: nop
IL_002b: ldstr "diss"
IL_0030: stloc.1
IL_0031: br.s IL_003b
IL_0033: ldstr "peace"
IL_0038: stloc.1
IL_0039: br.s IL_003b
IL_003b: ldloc.1
IL_003c: ret
} // end of method Rapper::op_Addition
執行步驟:
- 編譯器為op_Addition方法生成元數據方法定義項,并在定義項中設置了specialname標志,表明這是一個特殊方法。
- 編譯器發現代碼中出現+操作符時,會檢查是否有一個操作數的類型定義了名為op_Addition的specialname方法,而且該方法的參數兼容于操作數的類型。
- 如果存在這樣的方法,就生成調用它的代碼。
轉換操作符方法
有時需要將對象從一種類型轉換為另外一種全然不同的其他類型,此時便可以通過轉換操作符實現自定義類型轉換。同樣的,CLR規范要求轉換操作符重載方法必須是public和static的,并且要求參數類型和返回類型二者必有其一與定義轉換方法的類型相同。
在C#中使用implicit和explicit關鍵字定義隱式/顯示類型轉換。在Implicit或explicit關鍵字后,要指定operator關鍵字告訴編譯器該方法是一個轉換操作符。在operator之后,指定目標類型,而在參數部分指定源類型。
依舊沿用上面的示例,為Rapper類添加Rap方法,并為其添加無參構造函數。
public void Rap()
{Console.WriteLine("Rap");
}public Rapper()
{}
新增Dancer類,添加Dance方法,使用implicit/explicit關鍵字定義隱式/顯示類型轉換。
public class Dancer
{public void Dance(){Console.WriteLine("Breaking");}public static implicit operator Rapper(Dancer dancer){return new Rapper();}public static explicit operator Dancer(Rapper rapper){return new Dancer();}
}
修改Main方法:
class Program
{static void Main(string[] args){Rapper rapperA = new Rapper();Dancer dancerA = (Dancer)rapperA;dancerA.Dance(); //BreakingDancer dancerB = new Dancer();Rapper rapperB = dancerB;rapperB.Rap(); //RapConsole.ReadLine();}
}
最后,查看編譯器生成的IL代碼可以發現,將對象從一種類型轉換為另一種類型的方法總是叫做op_Implicit或op_Explicit。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 48 (0x30)
.maxstack 1
.locals init ([0] class ConsoleApplication5.Rapper rapperA,[1] class ConsoleApplication5.Dancer dancerA,[2] class ConsoleApplication5.Dancer dancerB,[3] class ConsoleApplication5.Rapper rapperB)
IL_0000: nop
IL_0001: newobj instance void ConsoleApplication5.Rapper::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: call class ConsoleApplication5.Dancer ConsoleApplication5.Dancer::op_Explicit(class ConsoleApplication5.Rapper)
IL_000d: stloc.1
IL_000e: ldloc.1
IL_000f: callvirt instance void ConsoleApplication5.Dancer::Dance()
IL_0014: nop
IL_0015: newobj instance void ConsoleApplication5.Dancer::.ctor()
IL_001a: stloc.2
IL_001b: ldloc.2
IL_001c: call class ConsoleApplication5.Rapper ConsoleApplication5.Dancer::op_Implicit(class ConsoleApplication5.Dancer)
IL_0021: stloc.3
IL_0022: ldloc.3
IL_0023: callvirt instance void ConsoleApplication5.Rapper::Rap()
IL_0028: nop
IL_0029: call string [mscorlib]System.Console::ReadLine()
IL_002e: pop
IL_002f: ret
} // end of method Program::Main
擴展方法
擴展方法已經在《從LINQ開始之LINQ to Objects(下)》一文中進行了詳細介紹,本篇就不再重復了。