C#基础知识梳理系列四:C#类成员:方法
世界上存在着男人和女人,如果没有某种东西把男人和女人连接起来构成“男女关系”,那么这些男人将立如树桩,仰天长叹,女人们将飘如小舟,荡无归处,整个世界毫无生机,自然离合。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);