# 原则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:使用动态接收匿名类型参数