C#建構函式、操作符過載以及自定義型別轉換
構造器
構造器(建構函式)是將型別的例項初始化的特殊方法。構造器可分為例項構造器和型別構造器,本節將詳細介紹有關內容。
例項構造器
顧名思義,例項構造器的作用就是對型別的例項進行初始化。如果類沒有顯示定義任何構造器,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 8 IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: stfld bool ConsoleApplication5.Rapper::real IL_0007: ldarg.0 IL_0008: call instance void [mscorlib]System.Object::.ctor() IL_000d: nop IL_000e: nop IL_000f: ldarg.0 IL_0010: ldarg.1 IL_0011: stfld string ConsoleApplication5.Rapper::name IL_0016: ldarg.0 IL_0017: ldarg.2 IL_0018: stfld int32 ConsoleApplication5.Rapper::age IL_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 8
IL_0000: ldstr "rap"
IL_0005: stsfld string ConsoleApplication5.Rapper::hobby
IL_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); //diss
Console.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(); //Breaking
Dancer dancerB = new Dancer();
Rapper rapperB = dancerB;
rapperB.Rap(); //Rap
Console.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(下)》一文中進行了詳細介紹,本篇就不再重複了。