GOTCHA #23 Copy Constructor hampers exensibilityYou use classes to model concepts in an object-oriented system and create instances of your classes throughout an application. You may be interested in making a copy of an object at runtime. How do you make such a copy? In C++, you don't have to do anything special; C++ gives you a default copy constructor, a constructor that takes an instance of the class as its parameter. But this is a mixed blessing (or is it a curse?). The default C++ copy constructor makes what is called a shallow copy; i.e., the contents of the source object are bit-wise copied into the other object. Deep copy is when not only the contents of an object are copied, but also the contents of the objects that this object refers to. A deep copy does not copy just one object; it copies a tree of objects. Whether you need a shallow copy or a deep copy depends on the relationship between the object and its contents. For instance, consider Example 3-7. Example 3-7. A class with different relationships with its contentsC# (CopyingObjects) public class Person { private int age; private Brain theBrain; private City cityOfResidence; } VB.NET (CopyingObjects) Public Class Person Private age as Integer Private theBrain as Brain Private cityOfResidence as City End Class If you make a copy of a Person object, you most likely want the new person to have a separate Brain, but may want to refer to (share) the City of the other person. From the object modeling point of view, the person aggregates the Brain but associates with the City. Generally you want to deep-copy the aggregated object, but you may want to shallow-copy the associated object, or just set it to null/Nothing. At the code level, you use a reference to represent both aggregation and association. There is a semantic mismatch between the object model and how it is expressed in the language. There is no way for the compiler or the runtime to figure out whether an object is being associated or aggregated. You have to implement the logic to properly copy an object. Without it, any effort to do so is just a guess, and probably not correct. This is the problem with the C++ approach. Unfortunately, C++ decided to err on the side of shallow copy. Instead of saying, "Hum, I have no idea how to make a copy so I won't even try," C++ decided, "Hum, I have no idea how to make a copy so I'll make a shallow copy." .NET decided to err on the side of caution. It says "I can't possibly make a copy of an object without the programmer clearly specifying the intent." So .NET doesn't provide a default copy constructor. Thus if you want to make a copy of an object, you just write your own copy constructor, right? Let's explore this further in Example 3-8. Example 3-8. Writing a copy constructorC# (CopyingObjects) //Brain.cs using System; namespace Copy { public class Brain { public Brain() {} public Brain(Brain another) { //Code to properly copy Brain can go here } public override string ToString() { return GetType().Name + ":" + GetHashCode(); } } } //Person.cs using System; namespace Copy { public class Person { private int theAge; private Brain theBrain; public Person(int age, Brain aBrain) { theAge = age; theBrain = aBrain; } public Person(Person another) { theAge = another.theAge; theBrain = new Brain(another.theBrain); } public override string ToString() { return "This is person with age " + theAge + " and " + theBrain; } } } //Test.cs using System; namespace Copy { class Test { [STAThread] static void Main(string[] args) { Person sam = new Person(1, new Brain()); Person bob = new Person(sam); // You rely on the copy constructor of Brain //to make a good deep copy Console.WriteLine(sam); Console.WriteLine(bob); } } } VB.NET (CopyingObjects) 'Brain.vb Public Class Brain Public Sub New() End Sub Public Sub New(ByVal another As Brain) ' Code to properly copy Brain can go here End Sub Public Overrides Function ToString() As String Return Me.GetType().Name & ":" & GetHashCode() End Function End Class 'Person.vb Public Class Person Private theAge As Integer Private theBrain As Brain Public Sub New(ByVal age As Integer, ByVal aBrain As Brain) theAge = age theBrain = aBrain End Sub Public Sub New(ByVal another As Person) theAge = another.theAge theBrain = New Brain(another.theBrain) ' You rely on the copy constructor of Brain ' to make a good deep copy End Sub Public Overrides Function ToString() As String Return "This is person with age " & _ theAge & " and " & _ theBrain.ToString() End Function End Class 'Test.vbModule Test Sub Main() Dim sam As New Person(1, New Brain) Dim bob As Person = New Person(sam) Console.WriteLine(sam) Console.WriteLine(bob) End Sub End Module This example has a Person class with theAge and theBrain as its members. Person has a constructor and a copy constructor. The Main() method in Test copies Person sam to Person bob. The output is shown in Figure 3-6. Figure 3-6. Output from Example 3-8When it prints the first Person (sam), the age is 1 and the Brain's hash code value is 1. When it prints the second Person (bob), which was copied from the instance sam, the age is 1 but the Brain's hash code is 2.
So, in Example 3-8 we created a copy of the Person with his own Brain. Have you solved the problem of properly copying the object? Not really, because the Person's copy constructor depends on the Brain class. It specifically creates an instance of Brain. What if you have a class that derives from Brain, as shown in Example 3-9? Example 3-9. Incorrect copyingC# (CopyingObjects) //SmarterBrain.cs using System; namespace Copy { public class SmarterBrain : Brain { public SmarterBrain() { } public SmarterBrain(SmarterBrain another) : base(another) { } } } //Test.cs using System; namespace Copy { class Test { [STAThread] static void Main(string[] args) { Person sam = new Person(1, new SmarterBrain()); Person bob = new Person(sam); Console.WriteLine(sam); Console.WriteLine(bob); } } } VB.NET (CopyingObjects) 'SmarterBrain.vb Public Class SmarterBrain Inherits Brain Public Sub New() End Sub Public Sub New(ByVal another As SmarterBrain) MyBase.New(another) End Sub End Class 'Test.vb Module Test Sub Main() Dim sam As New Person(1, New SmarterBrain) Dim bob As Person = New Person(sam) Console.WriteLine(sam) Console.WriteLine(bob) End Sub End Module SmarterBrain inherits from Brain. In the Main() method of Test you create an instance of SmarterBrain and send it to the Person object. The output after this enhancement is shown in Figure 3-7. Figure 3-7. Output after change in Example 3-9While the first Person instance (sam) has an instance of SmarterBrain, the copied instance (bob) is left with just a regular plain vanilla Brain. What went wrong? The Person's copy constructor is asking a new instance of Brain to be created regardless of the actual object referred to by theBrain. How about the fix in Example 3-10? Example 3-10. A fix?C# (CopyingObjects) public Person(Person another) { theAge = another.theAge; if(another.theBrain is SmarterBrain) { theBrain = new SmarterBrain( (SmarterBrain) another.theBrain); } else { theBrain = new Brain(another.theBrain); } } VB.NET (CopyingObjects) Public Sub New(ByVal another As Person) theAge = another.theAge If TypeOf another.theBrain Is SmarterBrain Then theBrain = New SmarterBrain( _ CType(another.theBrain, SmarterBrain)) Else theBrain = New Brain(another.theBrain) End If End Sub Here you have modified the copy constructor of the Person class to use Runtime Type Identification (RTTI). It seems to fix the problem. But what do you think about this solution? Not exactly elegant, is it? Actually, it's awful. It requires Person, which aggregates Brain, to know about all the subclasses of Brain. (The upside of code like this is job security. You will be around forever fixing and tweaking it.) As it stands, the Person class is not extensible for the addition of new types of Brains. It fails the Open-Closed Principle (OCP). Refer to [Martin03] for details on this and other object-oriented design principles.
How can you fix the code so it makes a proper copy of the object? The correct option is the prototype pattern, which is based on abstraction and polymorphism [Freeman04, Gamma95]. You depend on a prototypical instance to create a copy. This is discussed in the next gotcha, "Clone() has limitations" IN A NUTSHELLWriting a public copy constructor leads to extensibility problems. You should not use a public copy constructor in C++, Java, and the .NET languages. SEE ALSOGotcha #20, "Singleton isn't guaranteed process-wide," Gotcha #24, "Clone() has limitations," Gotcha #27, "Object initialization sequence isn't consistent," and Gotcha #28, "Polymorphism kicks in prematurely." |