Effective C# 原則27:避免使用ICloneable(譯)

來源:互聯網
上載者:User

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.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.