原则32:避免 ICloneable

优质
小牛编辑
129浏览
2023-12-01

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 是一样的!