Objective-C内存管理编程指南(6)实现对象复制

实现对象复制

本文介绍了实现NSCopying协议中的copyWithZone:方法的两种方式,都可以达到复制对象的目的。

有两种基本方式可以通过实现NSCopying协议copyWithZone:方法来创建副本。您可以使用allocinit...,也可以使用NSCopyObject。要选择一种更适合于您的类的方式,您需要考虑以下问题:

§ 我需要深拷贝还是浅拷贝?

§ 我从超类继承NSCopying的行为了吗?

这些内容将在以下章节进行介绍。

深拷贝与浅拷贝

一般来说,复制一个对象包括创建一个新的实例,并以原始对象中的值初始化这个新的实例。复制非指针型实例变量的值很简单,比如布尔,整数和浮点数。复制指针型实例变量有两种方法。一种方法称为浅拷贝,即将原始对象的指针值复制到副本中。因此,原始对象和副本共享引用数据。另一种方法称为深拷贝,即复制指针所引用的数据,并将其赋给副本的实例变量。

实例变量的set方法的实现应该能够反映出您需要使用的复制类型。如果相应的set方法复制了新的值,如下面的方法所示,那么您应该深拷贝这个实例变量:

- (void)setMyVariable:(id)newValue
{
 [myVariable autorelease];
 myVariable = [newValue copy];
}

如果相应的set方法保留了新的值,如下面的方法所示,那么您应该浅拷贝这个实例变量:

- (void)setMyVariable:(id)newValue
{
 [myVariable autorelease];
 myVariable = [newValue retain];
}

类似地,如果实例变量的set方法只是简单地将新的值赋给实例变量,而没有复制或保留它,那么您应该浅拷贝这个实例变量,正如下面的例子虽然这通常很罕见:

- (void)setMyVariable:(id)newValue
{
 myVariable = newValue;
}

独立副本

为了产生真正独立于原始对象的对象副本,必须对整个对象进行深拷贝。每个实例变量都必须被复制。如果实例变量本身具有实例变量,则它们也必须被复制,依此类推。在许多情况下,混合使用两种复制方式会更加有用。一般情况下,可以被视为数据容器的指针型实例变量往往被深拷贝,而更复杂的实例变量(如委托)则被浅拷贝。

@interface Product : NSObject <NSCopying>
{
 NSString *productName;
 float price;
 id delegate;
}
@end

例如,Product类继承了NSCopying。正如这个接口中所声明的,Product实例含有名称,价格和委托三个变量。

复制Product实例会产生productName的一份深拷贝,这是因为它表示一个扁平的数据值。但是,delegate实例变量是一个更复杂的对象,对于原始Product和副本Product都能够正常运行。因此,副本和原始对象应该共享这个委托。1表示了Product实例及其副本在内存中的映像。

1浅拷贝与深拷贝实例变量

productName的不同指针值说明,原始对象和副本各自都有自己的productName字符串对象。而delegate的指针值是相同的,这表明这两个product对象共享同一个对象作为它们的委托。

从超类继承 NSCopying

如果超类没有实现NSCopying,则您的类的实现必须复制它所继承的实例变量,以及那些在您的类中声明的实例变量。一般来说,完成这一任务的最安全的方式是使用alloc,init...set方法。

另一方面,如果您的类继承了NSCopying的行为,并声明了额外的实例变量,那么您也需要实现copyWithZone:。在这个方法中,调用超类的实现来复制继承的实例变量,然后复制其他新声明的实例变量。您要如何处理新的实例变量,取决于您对超类的实现的熟悉程度。如果超类使用了或者有可能使用过NSCopyObject,那么您必须有别于使用allocinit...函数的情况,用不同的方式处理实例变量。

使用“alloc, init...”方式

如果一个类没有继承NSCopying行为,则您应该使用alloc,init...set方法来实现copyWithZone:。例如,对于独立副本中提到的Product类,它的copyWithZone:方法可能会采用以下方式实现:

- (id)copyWithZone:(NSZone *)zone
{
 Product *copy = [[[self class] allocWithZone: zone]
 initWithProductName:[self productName]
 price:[self price]];
 [copy setDelegate:[self delegate]];
 return copy;
}

由于与继承实例变量相关的实现细节被封装在超类中,因此,一般情况下最好通过alloc,init...的方式实现NSCopying。这样一来就可以使用set方法中实现的策略来确定实例变量所需的复制类型。

使用 NSCopyObject()

如果一个类继承了NSCopying行为,则您必须考虑到超类的实现有可能会使用NSCopyObject函数。NSCopyObject通过复制实例变量的值而不是它们指向的数据,来创建对象的浅拷贝。例如,NSCellcopyWithZone:实现可能按照以下方式定义:

- (id)copyWithZone:(NSZone *)zone
{
 NSCell *cellCopy = NSCopyObject(self, 0, zone);
 /* Assume that other initialization takes place here. */
 cellCopy->image = nil;
 [cellCopy setImage:[self image]];
 return cellCopy;
}

在上面的实现中,NSCopyObject创建了原始cell对象的一份浅拷贝。这种行为适合于复制非指针型的实例变量,以及指向浅拷贝的非保留数据的指针型实例变量。指向保留对象的指针型实例变量还需要额外的处理。

在上面的copyWithZone:例子中,image是一个指向保留对象的指针。保留image的策略反映在下面setImage:存取方法的实现中。

- (void)setImage:(NSImage *)anImage
{
 [image autorelease];
 image = [anImage retain];
}

请注意,setImage:在重新对image赋值之前自动释放了image。如果上述copyWithZone:的实现没有在调用setImage:之前,显式地将副本的image实例变量设置为空,则副本和原始对象所引用的image将被释放,而没有被保留。

即使image指向了正确的对象,在概念上它也是未初始化的。不同于使用allocinit...创建的实例变量,这些未初始化的变量的值并不是nil值。您应该在使用它们之前显式地为这些变量赋予初始值。在这种情况下,cellCopyimage实例变量应该被设置为空,然后使用setImage:方法对它进行设置。

NSCopyObject的作用会影响到子类的实现。例如,NSSliderCell的实现可能会按照下面的方式复制一个新的titleCell实例变量。

- (id)copyWithZone:(NSZone *)zone
{
 id cellCopy = [super copyWithZone:zone];
 /* Assume that other initialization takes place here. */
 cellCopy->titleCell = nil;
 [cellCopy setTitleCell:[self titleCell]];
 return cellCopy;
}

其中,假设supercopyWithZone:方法完成这样的操作:

id copy = [[[self class] allocWithZone: zone] init];

超类的copyWithZone:方法被调用,以复制继承而来的实例变量。当您调用超类的copyWithZone:方法时,如果超类的实现有可能使用NSCopyObject,则假定新的对象的实例变量是未初始化的。您应该在使用这些实例变量之前显式地为它们赋值。在这个例子中,在setTitleCell:被调用之前,titleCell被显式地设置为nil

当使用NSCopyObject时,对象的保留计数的实现是另一个应该考虑的问题。如果一个对象将它的保留计数存储在一个实例变量中,则copyWithZone:的实现必须正确地初始化副本的保留计数。2说明了这个过程。

2复制过程中引用计数的初始化

2中的第一个对象表示内存中的一个Product实例。refCount中的值表明该对象已经被保留了3次。第二个对象是通过NSCopyObject产生的Product实例的一份副本。它的refCount值与原始对象一致。第三个对象表示从copyWithZone:返回的一份副本,它的refCount已被正确地初始化。copyWithZone:在使用NSCopyObject创建了副本之后,它将refCount实例变量的值赋为1copyWithZone:的调用者隐式地保留了该副本,并负责释放它。

复制可变的和不可变的对象

不可变的和可变的这一概念应用于某个对象时,不论原始对象是不可变的还是可变的,NSCopying总是产生不可变的副本。不可变的类可以非常有效地实现NSCopying。由于不可变的对象不会发生改变,因此没有必要复制它们。相反,NSCopying可以实现为retain原始对象。例如,对于一个不可变的字符串类,copyWithZone:可以按照下列方式实现。

- (id)copyWithZone:(NSZone *)zone {
 return [self retain];
}

要使用NSMutableCopying协议创建对象的可变副本。为了支持可变的复制,对象本身并不需要是可变的。该协议声明了mutableCopyWithZone:方法。通常,可变的复制是通过NSObject的便捷方法mutableCopy调用的,该方法调用了默认zonemutableCopyWithZone:

你可能感兴趣的:(Objective-C)