世界上存在着男人和女人,如果没有某种东西把男人和女人连接起来构成“男女关系”,那么这些男人将立如树桩,仰天长叹,女人们将飘如小舟,荡无归处,整个世界毫无生机,自然离合。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中会生成对那个指明的基类构造函数的调用。
先是调用基类的有参构造函数如果是一个静态类(对类使用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; } }