程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則27:避免使用ICloneable

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

編輯:關於C#

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接口。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved