构造器
构造器(构造函数)是将类型的实例初始化的特殊方法。构造器可分为实例构造器和类型构造器,本节将详细介绍有关内容。
实例构造器
顾名思义,实例构造器的作用就是对类型的实例进行初始化。如果类没有显示定义任何构造器,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(下)》一文中进行了详细介绍,本篇就不再重复了。