GOTCHA #24 Clone() has limitationsYou saw the extensibility problems posed by the use of a public copy constructor in Gotcha #23, "Copy Constructor hampers exensibility." How do you make a good copy of an object? If all you need is a shallow copy, do you have to write all the code yourself? No, .NET provides a MemberwiseClone() method that performs a shallow copy. However, to make sure this is not inadvertently used like the default copy constructor in C++, it is protected, not public. If you need to do a simple shallow copy, you provide a method that you implement using MemberwiseClone(). However, there are a couple of problems with this:
It is better to rely on polymorphism to create an object of the appropriate class. This is the intent of the System.ICloneable interface. You can implement ICloneable on the Brain class and call its Clone() method to copy the object, as shown in Example 3-11. Example 3-11. Using ICloneableC# (CopyingObjects) //Brain.cs using System; namespace Copy { public class Brain : ICloneable { //... #region ICloneable Members public object Clone() { return MemberwiseClone(); } #endregion } } //Person.cs //... public class Person { //... public Person(Person another) { theAge = another.theAge; theBrain = another.theBrain.Clone() as Brain; } } VB.NET (CopyingObjects) 'Brain.vb Public Class Brain Implements ICloneable '... Public Function Clone() As Object _ Implements System.ICloneable.Clone Return MemberwiseClone() End Function End Class 'Person.vb Public Class Person '... Public Sub New(ByVal another As Person) theAge = another.theAge theBrain = CType(another.theBrain.Clone(), Brain) End Sub End Class In this version, you implement ICloneable on the Brain class, and in its Clone() method do a shallow copy using MemberwiseClone(). For now, a shallow copy is good enough. The output of the program is shown in Figure 3-8. Figure 3-8. Output from Example 3-11The Clone() method copies the object correctly. The Person class is extensible to adding new types of Brain classes as well. Looks good. Are you done? Well, unfortunately, not yet! Let's think about this some more. Say the Brain has an identifier. (Brains don't usually, but just for the sake of this example, assume that the idea makes sense.) So, here is the Brain class with its identifier in Example 3-12. Example 3-12. A class with an identifierC# (CopyingObjects) //Brain.cs using System; namespace Copy { public class Brain : ICloneable { private int id; private static int idCount; public Brain() { id = System.Threading.Interlocked.Increment(ref idCount); } public Brain(Brain another) { //Code to properly copy Brain can go here } public override string ToString() { return GetType().Name + ":" + id; } #region ICloneable Members public object Clone() { return MemberwiseClone(); } #endregion } } VB.NET (CopyingObjects) 'Brain.vb Public Class Brain Implements ICloneable Private id As Integer Private Shared idCount As Integer Public Sub New() id = System.Threading.Interlocked.Increment(idCount) 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 & ":" & id End Function Public Function Clone() As Object _ Implements System.ICloneable.Clone Return MemberwiseClone() End Function End Class The Brain class has an id and a static/Shared field idCount. Within the constructor you increment (in a thread-safe manner) the idCount and store the value in the id field. You use this id instead of the hash code in the ToString() method. When you execute the code you get the output as in Figure 3-9. Figure 3-9. Output from Example 3-12Both the objects of SmarterBrain end up with the same id. Why's that? It's because the MemberwiseClone() method does not call any constructor. It just creates a new object by making a copy of the original object's memory. If you want to make id unique among the instances of Brain, you need to do it yourself. Let's fix the Clone() method, as shown in Example 3-13, by creating a clone using the MemberwiseClone() method, then modifying its id before returning the clone. The output after this change is shown in Figure 3-10. Example 3-13. Fixing the Clone() to maintain unique idC# (CopyingObjects) public object Clone() { Brain theClone = MemberwiseClone() as Brain; theClone.id = System.Threading.Interlocked.Increment(ref idCount); return theClone; } VB.NET (CopyingObjects) Public Function Clone() As Object _ Implements System.ICloneable.Clone Dim theClone As Brain = CType(MemberwiseClone(), Brain) theClone.id = _ System.Threading.Interlocked.Increment(idCount) Return theClone End Function Figure 3-10. Output from Example 3-13That looks better. But let's go just a bit further with this. If id is a unique identifier for the Brain object, shouldn't you make sure it doesn't change? So how about making it readonly? Let's do just that in Example 3-14. Example 3-14. Problem with readonly and Clone()C# (CopyingObjects) //Brain.cs // ... public class Brain : ICloneable { private readonly int id; private static int idCount; // ... VB.NET (CopyingObjects) 'Brain.vb Public Class Brain Implements ICloneable Private ReadOnly id As Integer Private Shared idCount As Integer '... As a result of this change, the C# compiler gives the error: A readonly field cannot be assigned to (except in a constructor or a variable initializer). In VB.NET, the error is: 'ReadOnly' variable cannot be the target of an assignment. A readonly field can be assigned a value at the point of declaration or within any of the constructors, but not in any other method. But isn't the Clone() method a special method? Yes, but not special enough. So if you have a readonly field that needs to have unique values, the Clone() operation will not work. Joshua Bloch discusses cloning very clearly in his book Effective Java [Bloch01]. He states, "... you are probably better off providing some alternative means of object copying or simply not providing the capability." He goes on to say, "[a] fine approach to object copying is to provide a copy constructor." Unfortunately, as you saw in Gotcha #23, "Copy Constructor hampers exensibility," the use of a copy constructor leads to extensibility issues. Here's the dilemma: I say copy constructors are a problem and Bloch says you can't use Clone(). So what's the answer? Providing a copy constructor is indeed a fine approach, as Bloch statesas long as it's with a slight twist. The copy constructor has to be protected and not public, and it should be invoked within Brain.Clone() instead of within the copy constructor of Person. The modified code is shown in Example 3-15. Example 3-15. A copy that finally worksC# (CopyingObjects) //Brain.cs using System; namespace Copy { public class Brain : ICloneable { private readonly int id; private static int idCount; public Brain() { id = System.Threading.Interlocked.Increment(ref idCount); } protected Brain(Brain another) { id = System.Threading.Interlocked.Increment(ref idCount); } public override string ToString() { return GetType().Name + ":" + id; } #region ICloneable Members public virtual object Clone() { return new Brain(this); } #endregion } } //SmarterBrain.cs using System; namespace Copy { public class SmarterBrain : Brain { public SmarterBrain() { } protected SmarterBrain(SmarterBrain another) : base(another) { } public override object Clone() { return new SmarterBrain(this); } } } VB.NET (CopyingObjects) 'Brain.vb Public Class Brain Implements ICloneable Private ReadOnly id As Integer Private Shared idCount As Integer Public Sub New() id = System.Threading.Interlocked.Increment(idCount) End Sub Protected Sub New(ByVal another As Brain) id = System.Threading.Interlocked.Increment(idCount) End Sub Public Overrides Function ToString() As String Return Me.GetType().Name & ":" & id End Function Public Overridable Function Clone() As Object _ Implements System.ICloneable.Clone Return New Brain(Me) End Function End Class 'SmarterBrain.vb Public Class SmarterBrain Inherits Brain Public Sub New() End Sub Protected Sub New(ByVal another As SmarterBrain) MyBase.New(another) End Sub Public Overrides Function Clone() As Object Return New SmarterBrain(Me) End Function End Class Now you have made the copy constructors of Brain and SmarterBrain protected. Also, you have made the Brain.Clone() method virtual/overridable. In it, you return a copy of the Brain created using the copy constructor. In the overridden Clone() method of SmarterBrain, you use the copy constructor of SmarterBrain to create a copy. When the Person class invokes theBrain.Clone(), polymorphism assures that the appropriate Clone() method in Brain or SmarterBrain is called, based on the real type of the object at runtime. This makes the Person class extensible as well. The output after the above modifications is shown in Figure 3-11. Figure 3-11. Output from Example 3-15A similar change to the Person class results in the code shown in Example 3-16. Example 3-16. Proper copying of Person classC# (CopyingObjects) //Person.cs using System; namespace Copy { public class Person : ICloneable { private int theAge; private Brain theBrain; public Person(int age, Brain aBrain) { theAge = age; theBrain = aBrain; } protected Person(Person another) { theAge = another.theAge; theBrain = another.theBrain.Clone() as Brain; } public override string ToString() { return "This is person with age " + theAge + " and " + theBrain; } #region ICloneable Members public virtual object Clone() { return new Person(this); } #endregion } } //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); Person bob = sam.Clone() as Person; Console.WriteLine(sam); Console.WriteLine(bob); } } } VB.NET (CopyingObjects) 'Person.vb Public Class Person Implements ICloneable 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 Protected Sub New(ByVal another As Person) theAge = another.theAge theBrain = CType(another.theBrain.Clone(), Brain) End Sub Public Overrides Function ToString() As String Return "This is person with age " & _ theAge & " and " & _ theBrain.ToString() End Function Public Overridable Function Clone() As Object _ Implements System.ICloneable.Clone Return New Person(Me) End Function End Class 'Test.vb Module Test Sub Main() Dim sam As New Person(1, New SmarterBrain) 'Dim bob As Person = New Person(sam) Dim bob As Person = CType(sam.Clone(), Person) Console.WriteLine(sam) Console.WriteLine(bob) End Sub End Module IN A NUTSHELLAvoid public copy constructors and do not rely on MemberwiseClone(). Invoke your protected copy constructor from within your Clone() method. Public copy constructors lead to extensibility problems. Using MemberwiseClone() can also cause problems if you have readonly fields in your class. A better approach is to write a Clone() method and have it call your class's protected copy constructor. SEE ALSOGotcha #20, "Singleton isn't guaranteed process-wide," Gotcha #23, "Copy Constructor hampers exensibility," Gotcha #27, "Object initialization sequence isn't consistent," and Gotcha #28, "Polymorphism kicks in prematurely." |