Effective C# 原则27:避免使用ICloneable(译)

Effective C# 原则27:避免使用ICloneable

Item 27: Avoid ICloneable

ICloneable看上去是个不错的主意:为一个类型实现ICloneable接口后就可以支持拷贝了。如果你不想支持拷贝,就不要实现它。
但你的对象并不是在一个“真空”的环境中运行,但考虑到对派生类的些影响,最好还是对ICloneable支持。一但某个类型支持ICloneable, 那么所有的派生类都必须保持一致,也就是所有的成员必须支持ICloneable接口或者提供一种机制支持拷贝。最后,支持深拷贝的对象,在创建设计时如果包含有网络结构的对象,会使拷贝很成问题。ICloneable也觉察到这个问题,在它的官方定义中有说明:它同时支持深拷贝和浅拷贝。浅拷贝是创建一个新的对象,这个新对象对包含当前对象中所有成员变量的拷贝。如果这些成员变量是引用类型的,那么新的对象与源对象包含了同样的引用。而深拷贝则可以很好的拷贝所有成员变量,引用类型也被递归的进行了拷贝。对于像整型这样的内置类型,深拷贝和浅拷贝是一样的结果。哪一种是我们的类型应该支持的呢?这取决于类型本身。但同时在一个类型中混用深拷贝和浅拷贝会导致很多不一致的问题。一但你涉及到ICloneable这个问题,这样的混用就很难解脱了。大多数时候,我们应该完全避免使用ICloneable,让类更简单一些。这样使用和实现都相对简单得多。

任何只以内置类型做为成员的值类型不必支持ICloneable; 用简单的赋值语句对结构的所有值进行拷贝比Clone()要高效得多。Clone()方法必须对返回类型进行装箱,这样才能强制转化成一个System.Object的引用。而调用者还得再用强制转化从箱子中取回这个值。我知道你已经有足够的能力这样做,但不要用Clone()函数来取代赋值语句。

那么,当一个值类型中包含一个引用类型时又会怎样呢?最常见的一种情况就是值类型中包含一个字符串:

public struct ErrorMessage
{
  private int errCode;
  private int details;
  private string msg;

 // details elided
}

字符串是一个特殊情况,因为它是一个恒定类。如果你指定了一个错误消息串,那么所有的错误消息类都引用到同一个字符串上。而这并不会导致任何问题,这与其它一般的引用类型是不一样的。如果你在任何一个引用上修改了msg变量,你会就为它重新创建了一个string对象(参见原则7)。

(译注:string确实是一个很有意思的类,很多C++程序员对这个类不理解,也很有一些C#程序对它不理解,导致很多的低效,甚至错误问题。应该好好的理解一下C#里的string(以及String和StringBulider之间的关系)这个类,这对于学好C#是很有帮助的。因为这种设计思想可以沿用到我们自己的类型中。)

一般情况,如果一个结构中包含了一个任意的引用类型,那么拷贝时的情况就复杂多了。这也是很少见的,内置的赋值语句会对结构进行浅拷贝,这样两个结构中的引用变量就引用到同个一个对象上。如果要进行深拷贝,那么你就必须对引用类型也进行拷贝,而且还要知道该引用类型上是否也支持用Clone()进行深拷贝。不管是哪种情况,你都不用对值类型添加对ICloneable的支持,赋值语句会对值类型创建一个新的拷贝。

一句概括值类型:没有任何理由要给一个值类型添加对ICloneable接口的支持! 好了,现在让我们再看看引用类型。引用类型应该支持ICloneable接口,以便明确的给出它是支持深拷贝还是浅拷贝。明智的选择是添加对ICloneable的支持,因为这样就明确的要求所有派生类也必须支持ICloneable。看下面这个简单的继承关系:

class BaseType : ICloneable
{
  private string _label = "class name";
  private int [] _values = new int [ 10 ];

  public object Clone()
  {
    BaseType rVal = new BaseType( );
    rVal._label = _label;
    for( int i = 0; i < _values.Length; i++ )
      rVal._values[ i ] = _values[ i ];
    return rVal;
  }
}

class Derived : BaseType
{
  private double [] _dValues = new double[ 10 ];

  static void Main( string[] args )
  {
    Derived d = new Derived();
    Derived d2 = d.Clone() as Derived;

    if ( d2 == null )
      Console.WriteLine( "null" );
  }
}

如果你运行这个程序,你就会发现d2为null。虽然Derived是从BaseType派生的,但从BaseType类继承的Clone()函数并不能正确的支持Derived类:它只拷贝了基类。BaseType.Clone()创建的是一个BaseType对象,不是派生的Derived对象。这就是为什么程序中的d2为null而不是派生的Derived对象。即使你克服了这个问题,BaseType.Clone()也不能正确的拷贝在Derived类中定义的_dValues数组。一但你实现了ICloneable, 你就强制要求所有派生类也必须正确的实现它。实际上,你应该提供一个hook函数,让所有的派生类使用你的拷贝实现(参见原则21)。在拷贝时,派生类可以只对值类型成员或者实现了ICloneable接口的引用类型成员进行拷贝。对于派生类来说这是一个严格的要求。在基类上实现ICloneable接口通常会给派生类添加这样的负担,因此在密封类中应该避免实现ICloneable 接口。

因此,当整个继承结构都必须实现ICloneable时,你可以创建一个抽象的Clone()方法,然后强制所有的派生类都实现它。

在这种情况下,你需要定义一个方法让派生类来创建基类成员的拷贝。可以通过定义一个受保护的构造函数来实现:

class BaseType
{
  private string _label;
  private int [] _values;

  protected BaseType( )
  {
    _label = "class name";
    _values = new int [ 10 ];
  }

  // Used by devived values to clone
  protected BaseType( BaseType right )
  {
    _label = right._label;
    _values = right._values.Clone( ) as int[ ] ;
  }
}

sealed class Derived : BaseType, ICloneable
{
  private double [] _dValues = new double[ 10 ];

  public Derived ( )
  {
    _dValues = new double [ 10 ];
  }

  // Construct a copy
  // using the base class copy ctor
  private Derived ( Derived right ) :
    base ( right )
  {
    _dValues = right._dValues.Clone( )
      as double[ ];
  }

  static void Main( string[] args )
  {
    Derived d = new Derived();
    Derived d2 = d.Clone() as Derived;
    if ( d2 == null )
      Console.WriteLine( "null" );
  }

  public object Clone()
  {
    Derived rVal = new Derived( this );
    return rVal;
  }
}

基类并不实现ICloneable接口; 通过提供一个受保护的构造函数,让派生类可以拷贝基类的成员。叶子类,应该都是密封的,必要它应该实现ICloneable接口。基类不应该强迫所有的派生类都要实现ICloneable接口,但你应该提供一些必要的方法,以便那些希望实现ICloneable接口的派生类可以使用。

ICloneable接口有它的用武之地,但相对于它的规则来说,我们应该避免它。对于值类型,你不应该实现ICloneable接口,应该使用赋值语句。对于引用类型来说,只有在拷贝确实有必要存在时,才在叶子类上实现对ICloneable的支持。基类在可能要对ICloneable 进行支持时,应该创建一个受保护的构造函数。总而言之,我们应该尽量避免使用ICloneable接口。
==========================

   

Item 27: Avoid ICloneable
ICloneable sounds like a good idea: You implement the ICloneable interface for types that support copies. If you don't want to support copies, don't implement it. But your type does not live in a vacuum. Your decision to support ICloneable affects derived types as well. Once a type supports ICloneable, all its derived types must do the same. All its member types must also support ICloneable or have some other mechanism to create a copy. Finally, supporting deep copies is very problematic when you create designs that contain webs of objects. ICloneable finesses this problem in its official definition: It supports either a deep or a shallow copy. A shallow copy creates a new object that contains copies of all member variables. If those member variables are reference types, the new object refers to the same object that the original does. A deep copy creates a new object that copies all member variables as well. All reference types are cloned recursively in the copy. In built-in types, such as integers, the deep and shallow copies produce the same results. Which one does a type support? That depends on the type. But mixing shallow and deep copies in the same object causes quite a few inconsistencies. When you go wading into the ICloneable waters, it can be hard to escape. Most often, avoiding ICloneable altogether makes a simpler class. It's easier to use, and it's easier to implement.

Any value type that contains only built-in types as members does not need to support ICloneable; a simple assignment copies all the values of the struct more efficiently than Clone(). Clone() must box its return so that it can be coerced into a System.Object reference. The caller must perform another cast to extract the value from the box. You've got enough to do. Don't write a Clone() function that replicates assignment.

What about value types that contain reference types? The most obvious case is a value type that contains a string:

public struct ErrorMessage
{
  private int errCode;
  private int details;
  private string msg;

 // details elided
}

string is a special case because this class is immutable. If you assign an error message object, both error message objects refer to the same string. This does not cause any of the problems that might happen with a general reference type. If you change the msg variable through either reference, you create a new string object (see Item 7).

The general case of creating a struct that contains arbitrary reference variables is more complicated. It's also far more rare. The built-in assignment for the struct creates a shallow copy, with both structs referring to the same object. To create a deep copy, you need to clone the contained reference type, and you need to know that the reference type supported a deep copy with its Clone() method. In either way, you don't add support for ICloneable to a value type; the assignment operator creates a new copy of any value type.

That covers value types: There is never a good reason to support the ICloneable interface in value types. Now let's move on to reference types. Reference types should support the ICloneable interface to indicate that they support either shallow or deep copying. You should add support for ICloneable judiciously because doing so mandates that all classes derived from your type must also support ICloneable. Consider this small hierarchy:

class BaseType : ICloneable
{
  private string _label = "class name";
  private int [] _values = new int [ 10 ];

  public object Clone()
  {
    BaseType rVal = new BaseType( );
    rVal._label = _label;
    for( int i = 0; i < _values.Length; i++ )
      rVal._values[ i ] = _values[ i ];
    return rVal;
  }
}

class Derived : BaseType
{
  private double [] _dValues = new double[ 10 ];

  static void Main( string[] args )
  {
    Derived d = new Derived();
    Derived d2 = d.Clone() as Derived;

    if ( d2 == null )
      Console.WriteLine( "null" );
  }
}

If you run this program, you will find that the value of d2 is null. The Derived class does inherit ICloneable.Clone() from BaseType, but that implementation is not correct for the Derived type: It only clones the base type. BaseType.Clone() creates a BaseType object, not a Derived object. That is why d2 is null in the test programit's not a Derived object. However, even if you could overcome this problem, BaseType.Clone() could not properly copy the _dValues array that was defined in Derived. When you implement ICloneable, you force all derived classes to implement it as well. In fact, you should provide a hook function to let all derived classes use your implementation (see Item 21). To support cloning, derived classes can add only member variables that are value types or reference types that implement ICloneable. That is a very stringent limitation on all derived classes. Adding ICloneable support to base classes usually creates such a burden on derived types that you should avoid implementing ICloneable in nonsealed classes.

When an entire hierarchy must implement ICloneable, you can create an abstract Clone() method and force all derived classes to implement it.

In those cases, you need to define a way for the derived classes to create copies of the base members. That's done by defining a protected copy constructor:

class BaseType
{
  private string _label;
  private int [] _values;

  protected BaseType( )
  {
    _label = "class name";
    _values = new int [ 10 ];
  }

  // Used by devived values to clone
  protected BaseType( BaseType right )
  {
    _label = right._label;
    _values = right._values.Clone( ) as int[ ] ;
  }
}

sealed class Derived : BaseType, ICloneable
{
  private double [] _dValues = new double[ 10 ];

  public Derived ( )
  {
    _dValues = new double [ 10 ];
  }

  // Construct a copy
  // using the base class copy ctor
  private Derived ( Derived right ) :
    base ( right )
  {
    _dValues = right._dValues.Clone( )
      as double[ ];
  }

  static void Main( string[] args )
  {
    Derived d = new Derived();
    Derived d2 = d.Clone() as Derived;
    if ( d2 == null )
      Console.WriteLine( "null" );
  }

  public object Clone()
  {
    Derived rVal = new Derived( this );
    return rVal;
  }
}

Base classes do not implement ICloneable; they provide a protected copy constructor that enables derived classes to copy the base class parts. Leaf classes, which should all be sealed, implement ICloneable when necessary. The base class does not force all derived classes to implement ICloneable, but it provides the necessary methods for any derived classes that want ICloneable support.

ICloneable does have its use, but it is the exception rather than rule. You should never add support for ICloneable to value types; use the assignment operation instead. You should add support for ICloneable to leaf classes when a copy operation is truly necessary for the type. Base classes that are likely to be used where ICloneable will be supported should create a protected copy constructor. In all other cases, avoid ICloneable.

原文地址:https://www.cnblogs.com/WuCountry/p/678080.html