文章目錄
支援複製,即用與現有執行個體相同的值建立類的新執行個體。
命名空間: System
程式集: mscorlib(在 mscorlib.dll 中)
文法
C#
[ComVisibleAttribute(true)]
public interface ICloneable
ICloneable 介面包含一個成員 Clone,它用於支援除 MemberwiseClone 所提供的複製之外的複製。
文法
C#
Object Clone()
Clone 既可作為深層副本實現,也可作為淺表副本實現。在深層副本中,所有的對象都是重複的;而在淺表副本中,只有頂級對象是重複的,並且頂級以下的對象包含引用。
結果複製必須與原始執行個體具有相同的類型或是原始執行個體的相容類型。
ICloneable聽起來是個好主意:可以為那些支援複製的類型實現ICloneable介面。如果不想支援複製,那就不要實現它。但是我們的類型並非活在真空中。讓一個類型支援ICloneable介面會影響它的衍生類別。一旦類型支援ICloneable介面,那麼它所有的衍生類別也都必須支援它。而且,其所有成員類型也都要支援ICloneable介面,或者有其他建立複製的機制。最後,當我們設計的類型包含交織成網狀的對象時,支援深複製將變得很困難。ICloneable介面在其官方的定義裡很巧妙地繞過了這個問題,其定義如下:ICloneable介面或者支援深複製(deep copy),或者支援淺複製(shallow copy)。淺複製指的是新對象包含所有成員變數的副本,如果成員變數為參考型別,那麼新對象將和原對象引用同樣的對象。深複製指的也是新對象包含所有成員變數的副本,但是所有參考型別的成員變數將被遞迴地複製。對於C#的內建類型,例如整數,深複製和淺複製產生的是同樣的結果。那麼我們的類型應該支援哪一個?這要根據具體類型而定。但是在同一個對象中混合淺複製和深複製會導致許多不一致的問題。當涉足ICloneable介面時,這樣的問題很難逃脫。大多數情況下,避免ICloneable介面反倒會獲得一個比較簡單的類——對類的客戶來講比較容易使用,對建立者來講也比較容易實現。
任何只包含內建類型成員的實值型別都不需要支援ICloneable介面;一個簡單的指派陳述式對struct的值所做的複製要比Clone()來得高效得多。Clone()必須對傳回值進行裝箱,才能轉換為一個System.Object引用。調用者則必須進行強制轉型才能擷取真正的值。實值型別預設的複製支援對我們來說已經足夠了。我們沒有必要再編寫Clone()函數來重複這項工作。
如果實值型別中包含參考型別呢?最明顯的例子是包含字串:
public struct ErrorMessage
{
private int errCode;
private int details;
private string msg;
// 忽略細節。
}
字串是一個特殊的例子,因為string是一個具有常量性的類。如果我們對ErrorMessage對象進行賦值,兩個ErrorMessage對象都將引用同一個字串。但這並不會導致任何問題,而這放到一個普通的參考型別就會出現問題。通過任何一個對象更改msg變數,都會建立一個新的string對象(參見條款7)。
更一般的情況——建立一個包含任意參考型別變數的struct——就比較複雜了。不過這種情況相當少見。C#語言為struct提供的內建賦值操作建立的是一個淺複製——即兩個struct引用的是同一個參考型別對象。要建立一個深複製,我們需要複製其內包含的參考型別,而且需要確知其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( );
條款27:避免ICloneable介面 154
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類中繼承了ICloneable.Clone()方法,但是繼承來的實現對Derived類型來講卻是不正確的,因為它僅僅複製了基類。BaseType.Clone()建立了一個BaseType對象,而非一個Derived對象。這就是測試程式中d2返回null的原因——它不是一個Derived對象。但是,即使我們能夠克服這個問題,BaseType.Clone()也不能對Derived中定義的_dValues數組進行正確的複製。當我們的類型實現了ICloneable介面,就會強制要求其所有衍生類別也實現ICloneable介面。實際上,這時候我們應該提供一個掛鈎函數(hook function)來允許所有衍生類別使用我們的實現(參見條款21)。為了支援複製,衍生類別只可以添加那些支援ICloneable介面的實值型別或參考型別成員變數。這對所有的衍生類別來說是一個非常嚴格的限制。因此我們說,為基類添加ICloneable介面支援通常會為其衍生類別帶來一些負擔,所以我們應該避免在非密封(nonsealed)類中實現ICloneable介面。
如果整個類層次必須實現ICloneable介面,我們可以建立一個抽象的Clone()方法,並強制要求所有的衍生類別實現它。
這時候,我們需要定義一種方式,使衍生類別可以建立基類成員的副本。這可以通過定義一個protected的複製構造器來實現:
class BaseType
{
private string _label;
private int [] _values;
protected BaseType( )
{
_label = "class name";
_values = new int [ 10 ];
}
// 供衍生類別用來做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 ];
}
// 使用基類的“複製構造器”構造一個副本。
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;
}
}
在上面的代碼中,我們的基類BaseType沒有實現ICloneable介面,但它提供了一個受保護的複製構造器,以使衍生類別可以複製其內的成員。如果有必要,“葉子類”——即那些密封類——可以實現ICloneable介面。我們的基類沒有強制要求所有的衍生類別實現ICloneable介面,但它為所有希望實現ICloneable介面的衍生類別提供了必要的方法支援。
ICloneable介面有其價值所在,但那都是特例,而非普遍的規則。對於實值型別來講,我們永遠都不需要支援ICloneable介面,使用預設的賦值操作就可以了。我們應該為那些確實需要複製操作的“葉子類”提供ICloneable介面支援。對於那些子類可能需要支援ICloneable介面的基類,我們應該為其建立一個受保護的複製構造器。除此之外,我們應該避免支援ICloneable介面