- Item 11: Understand .NET Resource Management
- Item 12: Prefer Member Initializers to Assignment Statements
- Item 13: Use Proper Initialization for Static Class Members
- Item 14: Minimize Duplicate Initialization Logic
- Item 15: Avoid Creating Unnecessary Objects
- Item 16: Never Call Virtual Functions in Constructors
- Item 17: Implement the Standard Dispose Pattern
Item 16: Never Call Virtual Functions in Constructors
Virtual functions exhibit strange behaviors during the construction of an object. An object is not completely created until all constructors have executed. In the meantime, virtual functions may not behave the way you’d like or expect. Examine the following simple program:
class B { protected B() { VFunc(); } protected virtual void VFunc() { Console.WriteLine("VFunc in B"); } } class Derived : B { private readonly string msg = "Set by initializer"; public Derived(string msg) { this.msg = msg; } protected override void VFunc() { Console.WriteLine(msg); } public static void Main() { var d = new Derived("Constructed in main"); } }
What do you suppose gets printed—“Constructed in main,” “VFunc in B,” or “Set by initializer”? Experienced C++ programmers would say, “VFunc in B.” Some C# programmers would say, “Constructed in main.” But the correct answer is “Set by initializer.”
The base class constructor calls a virtual function that is defined in its class but overridden in the derived class. At runtime, the derived class version gets called. After all, the object’s runtime type is Derived. The C# language definition considers the derived object completely available, because all the member variables have been initialized by the time any constructor body is entered. After all, all the variable initializers have executed. You had your chance to initialize all variables. But this doesn’t mean that you have necessarily initialized all your member variables to the value you want. Only the variable initializers have executed; none of the code in any derived class constructor body has had the chance to do its work.
No matter what, some inconsistency occurs when you call virtual functions while constructing an object. The C++ language designers decided that virtual functions should resolve to the runtime type of the object being constructed. They decided that an object’s runtime type should be determined as soon as the object is created.
There is logic behind this. For one thing, the object being created is a Derived object; every function should call the correct override for a Derived object. The rules for C++ are different here: The runtime type of an object changes as each class’s constructor begins execution. Second, this C# language feature avoids the problem of having a null method pointer in the underlying implementation of virtual methods when the current type is an abstract base class. Consider this variant base class:
abstract class B { protected B() { VFunc(); } protected abstract void VFunc(); } class Derived : B { private readonly string msg = "Set by initializer"; public Derived(string msg) { this.msg = msg; } protected override void VFunc() { Console.WriteLine(msg); } public static void Main() { var d = new Derived("Constructed in main"); } }
The sample compiles, because B objects aren’t created, and any concrete derived object must supply an implementation for VFunc(). The C# strategy of calling the version of VFunc() matching the actual runtime type is the only possibility of getting anything except a runtime exception when an abstract function is called in a constructor. Experienced C++ programmers will recognize the potential runtime error if you use the same construct in that language. In C++, the call to VFunc() in the B constructor would crash.
Still, this simple example shows the pitfalls of the C# strategy. The msg variable is immutable. It should have the same value for the entire life of the object. Because of the small window of opportunity when the constructor has not yet finished its work, you can have different values for this variable: one set in the initializer, and one set in the body of the constructor. In the general case, any number of derived class variables may remain in the default state, as set by the initializer or by the system. They certainly don’t have the values you thought, because your derived class’s constructor has not executed.
Calling virtual functions in constructors makes your code extremely sensitive to the implementation details in derived classes. You can’t control what derived classes do. Code that calls virtual functions in constructors is very brittle. The derived class must initialize all instance variables properly in variable initializers. That rules out quite a few objects: Most constructors take some parameters that are used to set the internal state properly. So you could say that calling a virtual function in a constructor mandates that all derived classes define a default constructor, and no other constructor. But that’s a heavy burden to place on all derived classes. Do you really expect everyone who ever uses your code to play by those rules? I didn’t think so. There is very little gain, and lots of possible future pain, from playing this game. In fact, this situation will work so rarely that it’s included in the FxCop and Static Code Analyzer tools bundled with Visual Studio.