构造函数主要用来初始化对象,它分为静态(static)构造函数和实例(instance)构造函数两种类别。大家应该都了解如何编写类的构造函数,这里只说明默认构造函数的作用,以及在类中保留默认构造函数的重要性。更准确地说,是保留空参数构造函数的重要性。
一、从一个简单的类开始
我们先来写一个类 A,代码如下:
public class A { public int Number; // 数字 public string Word; // 文本 }然后在 Test 类中实例化它:
public class Test { static void Main() { A a = new A(); // 实例化,A() 即为类 A 的默认构造函数 Console.WriteLine("Number = {0}\nWord = {1}", a.Number, a.Word); Console.Read(); } }输出的结果是:
Number = 0 Word =二、默认构造函数到底做了什么
在上面的示例中,我们并没有在 A 类中显式写出构造函数。当我们没有提供任何构造函数时,C# 会自动为类提供一个默认构造函数,其格式为:
public A() { // 空白的方法体 }这个默认构造函数看起来似乎根本没什么作用:没有接收任何输入参数,方法体也是空的。那么它真的是无所事事、可有可无的吗?回想一下构造函数的主要作用——初始化对象。再看输出结果:int类型的Number被初始化为默认值0,string类型的Word被初始化为默认值(空引用,输出为空白字符)。这些工作,正是由这个看似没起作用的默认构造函数完成的。
三、自己提供构造函数后,默认构造函数去哪了
很快,A 类的设计让我们并不满意,我们加强了它的能力,自己提供了构造函数:
public class A { public int Number; public string Word; public A(int number, string word) { Number = number; // 标记 1 Word = word; // 标记 2 } }Test 类也重新设计:
public class Test { static void Main() { A a = new A(100, "一百"); // 调用了新的带参数构造函数 Console.WriteLine("Number = {0}\nWord = {1}", a.Number, a.Word); Console.Read(); } }输出结果:
Number = 100 Word = 一百运行很顺利,结果正确。当我们提供了构造函数时,C# 认为我们已经意识到、并且有能力做好类的初始化工作,于是它撤销了默认构造函数,把控制权交到了我们手里。
那么我们自己的构造函数能胜任默认构造函数的工作吗?带着怀疑,我们做个尝试:将 A 类构造函数的方法体清空,即注释掉标记 1 和标记 2 两行,重新运行,输出结果是:
Number = 0 Word =这个结果和调用默认构造函数的结果是一样的,说明我们自己提供的构造函数同样能担当默认构造函数的工作。
四、继承场景下的构造函数调用顺序
不久以后,业务进一步扩展,我们要新建一个类,它继承自类 A,我们把它称为类 B:
public class A { public int Number; public string Word; public A(int number, string word) { Number = number; Word = word; } } public class B : A { public string Name; // 增加一个成员 public B(int number, string word) { Number = number; Word = word; } }修改 Test 类:
public class Test { static void Main() { B b = new B(100, "一百"); Console.WriteLine("Number = {0}\nWord = {1}", b.Number, b.Word); Console.Read(); } }运行后出错,错误信息是:“A”方法没有采用“0”个参数的重载。为什么会这样呢?看起来我们并没有什么错误:A 类提供了构造函数,B 类也提供了构造函数。
事实上,B 类要完成构造,必须先完成 A 类的构造。当我们采用public B(int number, string word)来构造 B 类时,由于没有明确指明继承的构造方法,C# 默认我们继承的是基类的默认构造函数,等价于:
public B(int number, string word) : base()采用这条构造函数时,C# 先调用类 A 的默认构造函数base()完成基类的构造,然后调用 B 类的构造函数完成针对 B 类构造的剩余工作。尝试修改上面的相应代码段为:
// 在类 A 中添加一个空参构造函数 public class A { // …… public A() { Number = 1; Word = "空参构造"; } // ……. } // 将类 B 构造函数改为继承 A 类空参构造 public B(int number, string word) : base()实际上,我是多此一举了:public B(……) : base()与public B(……)是一样的,未显式指明构造函数的继承体时,C# 就默认为base()。
输出结果:
Number = 100 Word = 一百注意看我们给 A 类添加的空参构造函数:我们在方法体内给Number赋值1,给Word赋值"空参构造"。然而从输出结果看,这些赋值都被覆盖了。这就说明,base()在前执行,而后才是public B(…..)的方法体。
五、错误原因与两种解决方案
回头来看我们那个错误的例子。遗憾的是,我们在类 A 中自己提供了构造函数,这导致 A 类的默认构造函数消失了;当 B 类从 A 类继承默认构造函数时,找不到入口,于是导致了如上的错误。
如果要避免错误,有两种方法:
- 在 A 类中添加一个无参数的构造函数;
- B 类构造函数显式继承 A 类的构造函数,例如
: base(number, word)。
六、为什么 C# 不能自动识别并继承
事情已经解释得很清楚了,不过也许还有个小问题:在 B 类中,我们提供了一个和 A 类构造函数结构相同的构造函数,为什么 C# 不能自动识别、自动继承呢?
道理很简单:C# 不能靠猜测来判断我们提供的参数究竟是要用作初始化基类,还是用于初始化继承类。还是来看一个例子:
public class A { public int Number; public string Word; public A(int number, string word) { Number = number; Word = word; } } public class B : A { public string Name; public B(int number, string word, string name) : base(number, word) { Number = number; Word = word; Name = name; } } public class Test { static void Main() { B b = new B(100, "一百", "初始化B的参数"); Console.WriteLine("Number = {0}\nWord = {1}\nName = {2}", b.Number, b.Word, b.Name); Console.Read(); } }输出结果是:
Number = 100 Word = 一百 Name = 初始化B的参数注意在 B 类的构造函数中,我们提供了三个参数:其中前两个用于初始化 A 类中的成员,第三个用于初始化 B 类的成员。虽然父类构造函数与子类构造函数的参数数量不一样,但由于我们明确指定了继承关系,它们很好地完成了工作。
七、小结
- 未显式定义构造函数时,C# 会自动提供一个无参的默认构造函数,负责把字段初始化为类型默认值。
- 一旦显式定义了任何构造函数,编译器就不再自动生成默认构造函数。
- 子类构造时会先调用基类构造函数,未显式指定时默认调用
base(),因此基类必须存在可访问的无参构造函数。 - 若基类只提供了带参构造函数,子类需通过
: base(参数)显式指定要调用的基类构造函数。