# 原則32:避免 ICloneable
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
IConeable 聽起來是一個很不錯的想法:你類型實現了 IConeable 接口然后就支持復制。如果你不想要支持復制,你就不需要實現它。但是你的類型不能在真空中存在。你支持 IConeable 的決定會影響它的子類。一旦一個類型支持 ICloneable ,它的所有子類也都必須支持 ICloneable 。它的所有成員的類型都必須支持 ICloneable 或者有其他機制去復制。最后,如果支持深度復制并且當你類型包含 web 對象就會很有問題。 ICloneable 的官方定義就給出了這個問題:它支持深復雜或淺復制。淺復制創建一個新對象包含所有成員變量的復制。如果這些成員變量是引用類型的,新對象引用和原來對象是同一個對象。深復制創建新對象同樣包含所有成員變量的復制。所有的引用類型都會被嵌套復制。對于內置變量,例如整數,深復制和淺復制產生的結果是一樣的。類型支持哪一個?這就依賴于具體的類型。但是在同一個對象混合深復制和淺復制會引起相當不一致的表現。當你去趟了 ICloneable 的渾水,這就再說難免了。大多數情況下,避免使用 ICloneable 使得類更簡單。它很容易使用,并且很容易實現。
任何只包含內置類的成員變量的值類型不需要支持 ICloneable ;簡單的賦值復制 struct 的所有值比 Clone() 更高效。Clone() 會對返回值進行封箱,以至于強制轉換為 System.Object 的引用。調用者必須進行強制類型轉換才能從箱中提取值。這樣做已經夠了。不要重寫 Clone() 函數來進行賦值復制。
如果值類型包含引用類型會怎么樣?最常見的例子是值類型包含一個 string :
```
public struct ErrorMessage
{
private int errCode;
private int details;
private string msg;
// details elided
}
```
string 是一個特殊例子因為它是不可變的類。如果你賦值一個 ErrorMessage 對象,兩個 ErrorMessage 對象會引用相同一個字符串。它不會引起一般引用類型的可能會出現的錯誤。如果你通過任何一個引用改變 msg 變量,會創建一個 string 對象(查看原則16)。
普遍的例子是創建一個包含任意引用變量的 struct 會更復雜。這也很少見。struct 內置的賦值創建淺復制,兩個 struct 會引用相同的對象。為了創建深復制,你需要克隆包含的引用類型對象,而且你需要知道這個引用類型通過 Clone() 方法來支持深復制。這樣,要做的工作如果包含的引用類型支持 ICloneable ,并且它的 Clone() 方法創建深復制。
下面我們開始討論引用類型。引用類型支持 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 繼承 ICloneable.Clone() ,但是實現卻對子類是不正確的:它只是克隆基類 BaseType.Clone() 創建基類對象,而不是子類對象。這就是測試程序中為什么 d2 為 null —— 它不是 Derived 對象。然而,即使你克服了這個問題, BaseType.Clone() 不能復制定義在 Derived 的 dValues 數組。所以當你實現 ICloneable ,你必須強制所有子類也都實現。實際上,你可以提供一個鉤子函數讓所有子類能有自己的實現(查看原則23)。為了支持克隆,子類只能添加實現了 ICloneable 的值類型或引用類型的成員變量。這是對于子類是非常嚴格的限制。在基類支持 ICloneable 增加了子類的負擔,所以你應該在非封閉的類避免實現 ICloneable 。
如果整個類的繼承結構都必須實現 ICloneable ,你可以差un感覺一個 abstract Clone() 方法,強制子類實現它。在這些例子,你還需要定義子類復制基類成員的方法。可以定義一個 protected 的復制構造函數:
```
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[];
}
public object Clone()
{
Derived rVal = new Derived(this);
return rVal;
}
}
```
基類沒有實現 ICloneable ;提供了 protected 的復制構造函數,讓子類能拿復制基類的部分。葉節點的類都是封閉的,當有必要的時候實現 ICloneable 。基類不會強制所有子類實現 ICloneable ,但是必須提供子類因支持 ICloneable 而需要的方法。
ICloneable 仍有它的用處,但是這是一個例外而不是指導規則。 .NET 框架更新支持泛型時,而沒有添加 ICloneable<T> 的支持是非常有意義的。你不應該對值類型添加 ICloneable 的支持;而是使用賦值操作。當復制操作對葉借點封閉類很重要,你就應該添加 ICloneable 支持。當基類支持 ICloneable 你就為此創建 protected 復制夠函數。對于其他的所有情況,避免使用 ICloneable 。
小結:
這個原則其實強調的重點是不管是值類型還是引用類型如果實現了 ICloneable 接口,這個類的成員變量和繼承結構也要實現 ICloneable ,才能做到深復制和淺復制的一致性。這點其實跟 Java 是一樣的!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2087490](/blog/2087490)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數