9.11 构造函数
构造函数是一类特殊的成员函数,它主要用于为对象分配内存空间,并对类的数据成员进行初始化,或者说设置实例对象到某一个初始状态(数据和状态相对应)。构造函数分为实例构造函数和静态构造函数两种,下面我们将分别对它们进行介绍。
实例构造函数
实例构造函数用于创建和初始化实例。在使用new运算符创建新的对象时,将调用类的实例构造函数,如代码清单9-24所示。
代码清单9-24 实例构造函数
class Car
{
//字段
private string name;
//构造函数
public Car()
{
name="Polo";
}
}
实例构造函数特点如下:
❑构造函数的名字必须和类同名;
❑构造函数不允许有返回类型;
❑构造函数可以具有0~n个参数;
❑构造函数可以重载,以提供初始化类的不同方法;
❑若在声明类时没有定义构造函数,则系统自动生成一个函数体为空的默认构造函数;
❑可以对实例构造函数使用public、protected、private修饰符;
❑引用基类构造函数时使用base()方法,如果有参数则将参数传入,如base(参数1,参数2……);
❑引用自身构造函数时使用this()方法,如果有参数则将参数传入,如this(参数1,参数2……)。
1.默认构造函数
如果没有为类提供任何构造函数,那么CLR将会自动提供一个构造函数作为默认构造函数,它是实例构造函数的一种,它的特点是:
❑没有参数;
❑构造函数体即方法体为空。
我们来看这样一个类,如代码清单9-25所示。
代码清单9-25 默认构造函数
//没有提供任何构造函数,此时CLR会自动提供一个默认构造函数
class Car
{
private string name;
public void DoSmothing(string name)
{
this.name=name;
}
public string Name
{
get
{
return name;
}
}
}
从上述代码可以看到,我们没有为类提供任何构造函数,那么对它进行编译,看看CLR是否自动生成了默认构造函数呢?从反编译后的Car类和代码清单9-26可以看出,CLR提供了一个默认构造函数.ctor,如图9-9所示。
图 9-9 Car类的CIL代码结构
其内容如代码清单9-26所示。
代码清单9-26 Car类默认构造函数.ctor的CIL代码
.method public hidebysig specialname rtspecialname instance void.ctor()cil managed
{
//Code size 7(0x7)
.maxstack 8
IL_0000:ldarg.0
IL_0001:call instance void[mscorlib]System.Object:.ctor()
IL_0006:ret
}//end of method Car:.ctor
这是由CLR生成的默认构造函数,它没有任何参数,它的任务就是简单地调用下基类(此处是System.Object,任何类都是System.Object的直接或间接的派生类,这里是直接派生自System.Object类)的构造函数(IL_0001行)。
现在,我们为Car类提供了一个构造函数,如代码清单9-27所示。
代码清单9-27 带构造函数的Car类
class Car
{
private string name;
//我们提供了一个构造函数,简单的为name自动赋值
public Car()
{
name="Polo";
}
public void DoSmothing(string name)
{
this.name=name;
}
public string Name
{
get
{
return name;
}
}
}
我们看看添加了构造函数以后,反编译出的CIL代码有何不同,如代码清单9-28所示。
代码清单9-28 添加了构造函数的Car类的CIL代码
.method public hidebysig specialname rtspecialname instance void.ctor()cil managed
{
//Code size 21(0x15)
.maxstack 8
IL_0000:ldarg.0
IL_0001:call instance void[mscorlib]System.Object:.ctor()
IL_0006:nop
IL_0007:nop
IL_0008:ldarg.0
IL_0009:ldstr"Polo"
IL_000e:stfld string ProgrammingCSharp4.Car:name
IL_0013:nop
IL_0014:ret
}//end of method Car:.ctor
我们可以发现,除了仍然调用基类的构造函数以外(IL_0001),新增了为字段赋值的代码(加粗部分),这也正是我们提供的构造函数所做的工作。
2.带参数的构造函数
如果想在创建类实例的同时就使用一些值进行初始化,该怎么办呢?答案就是构造函数,而且是带参数的构造函数,上节讲到的构造函数是默认构造函数,它没有参数。前面我们讲了,构造函数可以重载,那么我们在上例的基础上,再提供一个带参数的构造函数,先来看看C#代码,如代码清单9-29所示。
代码清单9-29 带参数的构造函数
class Car
{
private string name;
public Car()
{
name="Polo";
}
//带参数的构造函数
public Car(string name)
{
this.name=name;//使用this关键字以和参数的name分开
}
public void DoSmothing(string name)
{
this.name=name;
}
public string Name
{
get
{
return name;
}
}
}
那么,我们就可以在实例化Car类的同时,初始化类的字段name,如代码清单9-30所示。
代码清单9-30 使用带参数的构造函数初始化类的实例
class ClassExample
{
static void Main()
{
//使用带参数的构造函数实例化Car类
Car car=new Car("Benz");
Console.WriteLine(car.Name);
}
}
其输出如下所示:
Benz
下面,我们看看代码清单9-29编译生成的CIL代码,如图9-10所示。
图 9-10 Car类的CIL代码结构
可见,不带参数的默认构造函数仍然存在。其中,带参数的构造函数CIL代码如代码清单9-31所示。
代码清单9-31 构造函数的CIL代码
.method public hidebysig specialname rtspecialname instance void.ctor(string name)cil managed
{
//Code size 17(0x11)
.maxstack 8
IL_0000:ldarg.0
IL_0001:call instance void[mscorlib]System.Object:.ctor()
IL_0006:nop
IL_0007:nop
IL_0008:ldarg.0
IL_0009:ldarg.1
IL_000a:stfld string ProgrammingCSharp4.Car:name
IL_000f:nop
IL_0010:ret
}//end of method Car:.ctor
从代码清单9-31可以看出:
❑构造函数执行时,首先调用基类的默认构造函数(不带参数的)(IL_0001);
❑执行参数的赋值操作(IL_000a)。
3.静态构造函数
静态构造函数用于初始化任何静态数据,或用于执行仅需执行一次的特定操作。在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数。
下面来看一个静态构造函数示例,如代码清单9-32所示。
代码清单9-32 静态构造函数
class Car
{
private static string name;
//静态构造函数,仅执行一次
static Car()
{
Console.WriteLine(“静态构造函数调用了!”);
name="Polo";
}
//静态属性
public static string Name
{
get
{
return name;
}
}
}
静态构造函数有以下几个特性:
❑静态构造函数不能使用任何访问修饰符;
❑静态构造函数不能具有任何参数;
❑静态构造函数的执行时机,是在创建类的第一个实例,或者访问任何类的静态成员之前,自动执行的,并仅执行一次,以完成对静态成员的初始化工作;
❑不能直接调用静态构造函数;
❑程序中,用户无法控制执行静态构造函数的时机。
我们看一个实例,如代码清单9-33所示。
代码清单9-33 静态构造函数使用示例
class ClassExample
{
static void Main()
{
System.Console.WriteLine(Car.Name);
System.Console.WriteLine(Car.Name);
}
}
在这里,我们练习调用了两次Car类的静态成员属性Name,其输出如下:
静态构造函数调用了!
Polo
Polo
我们连续访问了两次Car对象的静态Name属性,只打印出了一次“静态构造函数调用了!”,这说明静态构造函数仅仅执行了一次。
同样,我们也观察一下静态构造函数生成的CIL代码,大家注意一下有什么不同,如图9-11所示。
图 9-11 静态函数生成的CIL代码结构
4.构造函数的可访问性
除了刚刚讲过的静态构造函数没有访问修饰符以外,实例构造函数都要有一个访问修饰符,如果没有表示隐式的private修饰符,构造函数是private,当前类只能通过自身进行实例化(构造函数虽然为private,但自身仍然是可以访问的)。除了private以外,构造函数还可以使用public以及protected修饰符,前者不需多讲,我们一直在用;后者表示构造函数是“受保护”的,如何保护呢?这意味着,当前类不能直接被实例化,而只能实例化当前类的子类,相关内容我们将在第10章具体阐述。
5.私有构造函数
下面,我们讨论私有构造函数。顾名思义,构造函数的访问修饰符为private,这意味着其他对象无法访问类的构造函数,也就无法直接将类进行实例化。因此,只能依赖类自身提供访问自身实例的途径。下面,我们看一个私有构造函数的例子,如代码清单9-34所示。
代码清单9-34 私有构造函数
class Car
{
private string name;
//私有构造函数,只能在内部调用,也就是说只能自己实例化自己
private Car()
{
Console.WriteLine(“私有实例构造函数调用了!”);
name="Polo";
}
//静态方法,用以返回类的示例
public static Car getInstance()
{
return new Car();
}
//属性
public string Name
{
get
{
return name;
}
}
}
在上述代码中,特意提供了一个静态的、返回Car类型的方法:getInstance(),它的责任是实例化自身,并将实例返回。建议大家参阅《设计模式之禅》中的单例模式,此模式是私有构造函数的典型应用之一。接下来我们看看如何使用这样一个构造函数私有的类,如代码清单9-35所示。
代码清单9-35 私有构造函数类的调用
class ClassExample
{
static void Main()
{
//通过调用静态方法getInstance()返回一个Car类型的实例
Car car=Car.getInstance();
System.Console.WriteLine(car.Name);
}
}
输出为:
私有实例构造函数调用了!
Polo