GOTCHA 24 Clone() has limitations


GOTCHA #24 Clone() has limitations

You 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:

  • You can't invoke MemberwiseClone() from within a copy constructor. This is because MemberwiseClone() creates and returns an object, and there is no way to return this from a copy constructor.

  • MemberwiseClone() does not use a constructor to create an object and can't deal with objects that have readonly fields, as discussed later in this gotcha.

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 ICloneable

C# (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-11


The 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 identifier

C# (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-12


Both 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 id

C# (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-13


That 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 works

C# (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-15


A similar change to the Person class results in the code shown in Example 3-16.

Example 3-16. Proper copying of Person class

C# (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 NUTSHELL

Avoid 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 ALSO

Gotcha #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."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net