本文介绍了实现NSCopying
协议中的copyWithZone:方法的两种方式,都可以达到复制对象的目的。
有两种基本方式可以通过实现NSCopying协议的copyWithZone:
方法来创建副本。您可以使用alloc
和init...
,也可以使用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实例及其副本在内存中的映像。
productName
的不同指针值说明,原始对象和副本各自都有自己的productName
字符串对象。而delegate
的指针值是相同的,这表明这两个product对象共享同一个对象作为它们的委托。
如果超类没有实现NSCopying
,则您的类的实现必须复制它所继承的实例变量,以及那些在您的类中声明的实例变量。一般来说,完成这一任务的最安全的方式是使用alloc
,init...
和set
方法。
另一方面,如果您的类继承了NSCopying
的行为,并声明了额外的实例变量,那么您也需要实现copyWithZone:
。在这个方法中,调用超类的实现来复制继承的实例变量,然后复制其他新声明的实例变量。您要如何处理新的实例变量,取决于您对超类的实现的熟悉程度。如果超类使用了或者有可能使用过NSCopyObject
,那么您必须有别于使用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方法中实现的策略来确定实例变量所需的复制类型。
如果一个类继承了NSCopying
行为,则您必须考虑到超类的实现有可能会使用NSCopyObject
函数。NSCopyObject
通过复制实例变量的值而不是它们指向的数据,来创建对象的浅拷贝。例如,NSCell
的copyWithZone:
实现可能按照以下方式定义:
- (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
指向了正确的对象,在概念上它也是未初始化的。不同于使用alloc
和init...
创建的实例变量,这些未初始化的变量的值并不是nil
值。您应该在使用它们之前显式地为这些变量赋予初始值。在这种情况下,cellCopy
的image
实例变量应该被设置为空,然后使用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; |
} |
其中,假设super
的copyWithZone
:方法完成这样的操作:
id copy = [[[self class] allocWithZone: zone] init]; |
超类的copyWithZone:
方法被调用,以复制继承而来的实例变量。当您调用超类的copyWithZone:
方法时,如果超类的实现有可能使用NSCopyObject
,则假定新的对象的实例变量是未初始化的。您应该在使用这些实例变量之前显式地为它们赋值。在这个例子中,在setTitleCell:
被调用之前,titleCell
被显式地设置为nil
。
当使用NSCopyObject
时,对象的保留计数的实现是另一个应该考虑的问题。如果一个对象将它的保留计数存储在一个实例变量中,则copyWithZone:
的实现必须正确地初始化副本的保留计数。图2说明了这个过程。
图2中的第一个对象表示内存中的一个Product实例。refCount
中的值表明该对象已经被保留了3次。第二个对象是通过NSCopyObject
产生的Product实例的一份副本。它的refCount
值与原始对象一致。第三个对象表示从copyWithZone:
返回的一份副本,它的refCount
已被正确地初始化。copyWithZone:
在使用NSCopyObject
创建了副本之后,它将refCount
实例变量的值赋为1。copyWithZone:
的调用者隐式地保留了该副本,并负责释放它。
当“不可变的和可变的”这一概念应用于某个对象时,不论原始对象是不可变的还是可变的,NSCopying
总是产生不可变的副本。不可变的类可以非常有效地实现NSCopying
。由于不可变的对象不会发生改变,因此没有必要复制它们。相反,NSCopying
可以实现为retain
原始对象。例如,对于一个不可变的字符串类,copyWithZone:
可以按照下列方式实现。
- (id)copyWithZone:(NSZone *)zone { |
return [self retain]; |
} |
要使用NSMutableCopying
协议创建对象的可变副本。为了支持可变的复制,对象本身并不需要是可变的。该协议声明了mutableCopyWithZone:
方法。通常,可变的复制是通过NSObject
的便捷方法mutableCopy
调用的,该方法调用了默认zone的mutableCopyWithZone:
。