GOTCHA #28 Polymorphism kicks in prematurelyPolymorphism is the most cherished feature in object-oriented programming. An important tenet in object modeling is that objects be kept in a valid state at all times. Ideally, you should never be able to invoke methods on an object until it has been fully initialized. Unfortunately in .NET, it isn't difficult to violate this with the use of polymorphism. Unlike C++, in .NET polymorphism kicks in even before the execution of the constructor has completed. This behavior is similar to Java. Let's review polymorphism for a moment. It assures that the virtual/overridable method that is called is based on the real type of the object, and not just the type of the reference used to invoke it. For instance, say foo() is a virtual/overridable method on a base class, and a class that derives from the base overrides that method. Assume also that you have two references baseReference and derivedReference of the base type and derived type. Let both of these references actually refer to the same instance of the derived class. Now, regardless of how the method is called, either as baseReference.foo() or derivedReference.foo(), the same method foo() in the derived class is invoked. This is due to the effect of polymorphism or dynamic binding. While this sounds great, the problem is that polymorphism enters into the picture before the derived class's constructor is even called. Consider Example 3-21. Example 3-21. Polymorphism during constructionC# (PolymorphismTooSoon) //Room.cs using System; namespace ProblemPolymorphismConstruction { public class Room { public void OpenWindow() { Console.WriteLine("Room window open"); } public void CloseWindow() { Console.WriteLine("Room window closed"); } } } //ExecutiveRoom.cs. using System; namespace ProblemPolymorphismConstruction { public class ExecutiveRoom : Room { } } //Employee.cs using System; namespace ProblemPolymorphismConstruction { public class Employee { public Employee() { Console.WriteLine("Employee's constructor called"); Work(); } public virtual void Work() { Console.WriteLine("Employee is working"); } } } //Manager.cs using System; namespace ProblemPolymorphismConstruction { public class Manager : Employee { private Room theRoom = null; private int managementLevel = 0; public Manager(int level) { Console.WriteLine("Manager's constructor called"); managementLevel = level; if (level < 2) theRoom = new Room(); else theRoom = new ExecutiveRoom(); } public override void Work() { Console.WriteLine("Manager's work called"); theRoom.OpenWindow(); base.Work(); } } } //User.cs using System; namespace ProblemPolymorphismConstruction { class User { static void Main(string[] args) { Console.WriteLine("Creating Manager"); Manager mgr = new Manager(1); Console.WriteLine("Done"); } } } VB.NET (PolymorphismTooSoon) 'Room.vb Public Class Room Public Sub OpenWindow() Console.WriteLine("Room window open") End Sub Public Sub CloseWindow() Console.WriteLine("Room window closed") End Sub End Class 'ExecutiveRoom.vb Public Class ExecutiveRoom Inherits Room End Class 'Employee.vb Public Class Employee Public Sub New() Console.WriteLine("Employee's constructor called") Work() End Sub Public Overridable Sub Work() Console.WriteLine("Employee is working") End Sub End Class 'Manager.vb Public Class Manager Inherits Employee Private theRoom As Room = Nothing Private managementLevel As Integer = 0 Public Sub New(ByVal level As Integer) Console.WriteLine("Manager's constructor called") managementLevel = level If level < 2 Then theRoom = New Room Else theRoom = New ExecutiveRoom End If End Sub Public Overrides Sub Work() Console.WriteLine("Manager's work called") theRoom.OpenWindow() MyBase.Work() End Sub End Class 'User.vb Module User Sub Main() Console.WriteLine("Creating Manager") Dim mgr As New Manager(1) Console.WriteLine("Done") End Sub End Module In the example given above, you have a Room class with OpenWindow() and CloseWindow() methods. The ExecutiveRoom derives from Room, but does not have any additional functionality as yet. The Employee has a constructor that invokes its Work() method. The Work() method, however, is declared virtual/overridable in the Employee class. In the Manager class, which inherits from Employee, you have a reference of type Room. Depending on the Manager's level, in the constructor of the Manager, you assign the theRoom reference to either an instance of Room or an instance of ExecutiveRoom. In the overridden Work() method in the Manager class, you invoke the method on theRoom to open the window and then invoke the base class's Work() method. Looks reasonable so far, doesn't it? But when you execute this program you get a NullReferenceException as shown in Figure 3-18. Figure 3-18. Exception from Example 3-21Notice that in the creation of the Manager object, the Employee's constructor is called first. From the Employee's constructor, the call to Work() polymorphically calls Manager.Work(). Why? In the Employee constructor, even though the self reference this/Me is of type Employee, the real instance is of type Manager. But at this point, the constructor of Manager has not been invoked. As a result, the reference theRoom is still null/Nothing. The Work() method, however, assumes that the object has been constructed and tries to access theRoom. Hence the NullReferenceException. Ideally, no method should ever be called on an object until its constructor has completed. However, the above example shows that there are situations where this can happen. As a side note, if you initialize theRoom at the point of declaration to a Room instance, you half-fix the problem. The C# code will run fine, but the VB.NET code will still throw the exception. The reason for this? The difference in the sequence of initialization between the two languages, as discussed in Gotcha #27, "Object initialization sequence isn't consistent." IN A NUTSHELLUnderstand the consequence of calling virtual/overridable methods from within a constructor. If you need to further initialize your object, provide an Init() method that users of your object can call after the constructor completes. This even has a name: two-phase construction. SEE ALSOGotcha #23, "Copy Constructor hampers exensibility," Gotcha #24, "Clone() has limitations," Gotcha #27, "Object initialization sequence isn't consistent," Gotcha #43, "Using new/shadows causes "hideous hiding"," Gotcha #44, "Compilers are lenient toward forgotten override/overrides," Gotcha #45, "Compilers lean toward hiding virtual methods," and Gotcha #47, "Signature mismatches can lead to method hiding." |