Item 27 避免使用ICloneable接口

  实现ICloneable接口,看起来是个不错的选择,想要类型支持拷贝,就实现ICloneable,不想支持拷贝,就不实现ICloneable。但是,大家仔细想一想,你的对象并不是在一个独立的环境中运行,需要考虑到对派生类的影响,基类已经实现了ICloneable接口,派生类也继承了基类的Clone方法,所以派生类最好也支持ICloneable接口,这样所有派生类都应保持一致,所以所有派生类都应实现ICloneable接口,还需要考虑到类的成员都必须支持ICloneable接口或提供一种机制支持拷贝,如果支持深拷贝的对象包含有网状结构的对象,就会使拷贝很成问题。
   这里谈到了深拷贝,对应的是浅拷贝。所谓浅拷贝,是创建一个新对象,然后将原对象的所有成员拷贝到新对象中,如果某成员是引用类型,那么仅仅拷贝引用给新对象,也就是说新,旧对象的那个成员引用的是同一个对象。而深拷贝是拷贝所有成员并对引用类型的对象进行递归的拷贝,即对引用的对象也进行拷贝(而不仅仅是引用的拷贝,并且是递归下去,直到递归到只有值类型成员的拷贝)。像值类型和String类型,深拷贝和浅拷贝效果一样,都是完全复制,得到一个与原对象无关新对象(只有内容一样,没有其它关联,对两者中任何一个的修改,不会影响到另一个对象)。
   那么当我们定义的类型实现了ICloneable接口时,我们应该实现深拷贝还是浅拷贝呢,这取决于类型本身,但同时在一个类型中混用深拷贝和浅拷贝会导致很多不一致问题,一旦实现了ICloneable接口,这种混用就很难避免,所以定义类型时应该尽量避免实现ICloneable接口,让类更简单一些,使用和实现都相对简单一些。

  任何只以值类型和String类型对象作为成员的值类型都不用支持ICloneable接口,用简单的赋值语句要比Clone方法高效得多,Clone方法要对返回的对象进行装箱,才能强制返回一个System.Object对象,而调用者使用对象时又要对其进行拆箱,这又是何苦呢?例如:

public struct ErrorMessage
    {
        
private int _errorCode;
        
private int _details;
        
private string _msg;
    }
这时可能大家会产生一个疑问:string类型是引用类型,为什么一个简单的赋值语句就能实现_msg的深拷贝,而不用显示的去实现,这是因为string类型的恒定性,对string类型对象的任何操作都会产生一个新的string对象,并返回其引用。string确实是一个很有意思的类,很多C++程序员对这个类不理解,也很有一些C#程序对它不理解,导致很多的低效,甚至错误问题。应该好好的理解一下C#里的string(以及String和StringBulider之间的关系)这个类,这对于学好C#是很有帮助的。因为这种设计思想可以沿用到我们自己的类型中。
  如果一个结构体包含一个一般的引用类型(除了字符串外),那么这时拷贝的情况就复杂的多。内置的赋值操作只会对结构体进行浅拷贝,这样两个结构体内部的引用类型的成员引用的是同一个对象。如果要进行深拷贝,那就要实现ICloneable接口,并且在Clone方法中对该成员变量引用的对象进行深拷贝,这就必须知道该对象是否实现ICloneable接口,并在该对象的Clone()方法中也支持深拷贝。当然,由于这种在值类型中包含引用类型成员的情况比较少见,所以我们还是没有理由对值类型实现ICloneable接口。
  而对于引用类型来说,只在确实有必要支持拷贝时,才在叶子类(继承体系的末端,即标识为sealed的类)中实现ICloneable接口。为什么说只能在叶子类中实现ICloneable接口呢?看看下面这个例子:
public class BaseType : ICloneable
    {
        
private string _label = "PeterLau";
        
private int[] _values = new int[10];

        
public object Clone()
        {
            BaseType baseType 
= new BaseType();
            baseType._label 
= this._label;
            baseType._values 
= this._values;
            
return baseType;
        }
    }

    
public class DerivedType : BaseType
    {
        
private double[] _dValues=new double[10];

        
static void Main()
        {
            DerivedType derivedType1 
= new DerivedType();
            DerivedType derivedType2 
= derivedType1.Clone() as DerivedType;
            
if (derivedType2 == null)
                Console.WriteLine(
"null");
        }
    }
上述这段代码的输出是"null",很明显当子类对象调用Clone()方法时,其实是调用的基类的Clone()方法,该方法返回的是基类的对象,当使用as转换为子类对象时当然返回的是null。即使你解决了这一个问题,基类的Clone()方法也不可能拷贝子类的_dValues字段。所以一旦你的类型实现了ICloneable接口,那么就强迫你的派生类也要正确的实现ICloneable接口(因为子类继承了基类的Clone方法,在客户端可以通过子类对象调用Clone方法,但是调用返回的结果又是错的),在基类中实现ICloneable接口会给派生类带来这样的负担,所以当你的引用类型要实现ICloneable接口时,最好将你的类型定义为sealed。如果你的基类实现了ICloneable接口,于是你的整个继承体系都要实现ICloneable接口时,这时你可以在基类中定义一个抽象的Clone方法,强迫所有派生类提供Clone方法的实现。当然这是出于安全考虑,最好还是不要在非密封类中实现ICloneable接口。
  当在密封子类中实现ICloneable接口时,需要在基类中定义一个protected的构造函数,以便在子类的Clone方法中拷贝基类成员。例如:
public class BaseType
    {
        
private string _label = "PeterLau";
        
private int[] _values = new int[10];

        
public BaseType()
        { }

        
protected BaseType(BaseType baseType)
        {
            _label 
= baseType._label;
            _values 
= baseType._values.Clone() as int[];
        }
    }

    
public class DerivedType : BaseType,ICloneable
    {
        
private double[] _dValues=new double[10];

        
public DerivedType()
        {
            
        }
        
        
private DerivedType(DerivedType right)
            : 
base(right)
        {
            _dValues 
= right._dValues;
        }

        
public object Clone()
        {
            DerivedType derivedType 
= new DerivedType(this);
            
return derivedType;
        }

        
static void Main()
        {
            DerivedType derivedType1 
= new DerivedType();
            DerivedType derivedType2 
= derivedType1.Clone() as DerivedType;
            
if (derivedType2 == null)
                Console.WriteLine(
"null");
        }
    }

基类并不实现ICloneable接口; 通过提供一个受保护的构造函数,让派生类可以拷贝基类的成员。叶子类,应该都是密封的,必要它应该实现ICloneable接口。基类不应该强迫所有的派生类都要实现ICloneable接口,但你应该提供一些必要的方法,以便那些希望实现ICloneable接口的派生类可以使用。

ICloneable接口有它的用武之地,但相对于它的规则来说,我们应该避免它。对于值类型,你不应该实现ICloneable接口,应该使用赋值语句。对于引用类型来说,只有在拷贝确实有必要存在时,才在叶子类上实现对ICloneable的支持。基类在可能要对ICloneable 进行支持时,应该创建一个受保护的构造函数。总而言之,我们应该尽量避免使用ICloneable接口。

posted @ 2009-09-28 23:28  PeterLau  阅读(449)  评论(1)    收藏  举报