More Effective C# 24.避免使用 ICloneable,因為它限制你的設計選擇 More Effective C# 24.避免使用 ICloneable,因為它限制你的設計選擇(Avoid ICloneable because it limits your design choices)

這個做法建議不要實做 ICloneable 介面,還有 Deep CopyShallow Copy 的概念。

下面這個介面是 ICloneable 的所有內容,他只需要實做一個 Clone 方法,但這有一個問題就是這個介面沒有明確說明 該實做 Deep Copy 還是 Shallow Copy,所以每個人就會根據自己類別的需求來實做,這種不規範就會就成問題。

public interface ICloneable
{
    object Clone();
}

Shallow Copy: 當成員為 Value Type 的時候複製新內容,但如果為 Reference Type 只會複製參考。 Deep Copy: 不管 Value TypeReference Type 都會複製一份新的內容。

由於 Shallow Copy 在複製 Reference Type 的時候只會複製參考,因此複製後的物件會與舊物件共享同一份參考。 Deep Copy 則會遞迴複製所有物件,確保新物件與原物件完全獨立。

參考這段程式碼,輸出的結果為 null,這是因為 Derived 類別透過繼承使用 Clone 方法,但是新複製的型別為 BaseType 所以不可能轉換成 Derived 類型,最後導致 d2 變數為 null,同時寫基底類別的 Clone 方法的時候不可能知道衍生類別會增加什麼成員, 這也代表你只要在基底類別實做了 ICloneable 介面,就必須在衍生類別也實做它才能做到正確的複製。

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

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];
}

要解決這個問題可以建立 Copy Constructor,它是一種能用來複製的建構函式,通常需要傳入一個物件並建造出一個相等的物件。 這樣就能避免在基底類別實做 ICloneable 介面,之後在衍生類別只要呼叫這個 Copy Constructor 就能做到複製的效果。

class BaseType
{
    private string label;
    private int[] values;

    protected BaseType() // 預設建構子
    {
        label = "class name";
        values = new int[10];
    }

    // Copy Constructor,供衍生類別使用
    protected BaseType(BaseType other)
    {
        label = other.label;
        values = (int[])other.values.Clone();
    }
}

sealed class Derived : BaseType, ICloneable
{
    private double[] dValues;

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

    private Derived(Derived other) : base(other)
    {
        dValues = (double[])other.dValues.Clone();
    }

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

Summary

實做 ICloneable 介面會影響到繼承,導致衍生類別都需要額外實做 ICloneable 介面保證功能能正常運作,由於不能確定使用者提供的 是 Deep Copy 還是 Shallow Copy 所以建議是完全不要實做 ICloneable 介面比較好,改成用 Copy Constructor 明確進行複製也能達到同樣的效果。