复制运算符
典型cloning
,copy
,copyWithZone:
,NSCopyObject()
在 C++ 中,定义复制运算符和相关的操作是很重要的。在 Objective-C 中,运算法是不允许重定义的,所能做的就是要求提供一个正确的复制函数。
克隆操作在 Cocoa 中要求使用NSCopying
协议实现。该协议要求一个实现函数:
-(id) copyWithZone:(NSZone*)zone;
这个函数的参数是一个内存区,用于指明需要复制那一块内存。Cocoa 允许使用不同的自定义区块。大多数时候默认的区块就已经足够,没必要每次都单独指定。幸运的是,NSObject
有一个函数
-(id) copy;
封装了copyWithZone:
,直接使用默认的区块作为参数。但它实际相当于NSCopying
所要求的函数。另外,NSCopyObject()
提供一个不同的实现,更简单但同样也需要注意。下面的代码没有考虑NSCopyObject()
:
// 如果父类没有实现 copyWithZone:,并且没有使用 NSCopyObject() -(id) copyWithZone:(NSZone*)zone { // 创建对象 Foo* clone = [[Foo allocWithZone:zone] init]; // 实例数据必须手动复制 clone->integer = self->integer; // "integer" 是 int 类型的 // 使用子对象类似的机制复制 clone->objectToClone = [self->objectToClone copyWithZone:zone]; // 有些子对象不能复制,但是可以共享 clone->objectToShare = [self->objectToShare retain]; // 如果有设置方法,也可以使用 [clone setObject:self->object]; return clone; }
注意,我们使用的是allocWithZone:
而不是alloc
。alloc
实际上封装了allocWithZone:
,它传进的是默认的 zone。但是,我们应该注意父类的copyWithZone:
的实现。
// 父类实现了 copyWithZone:,并且没有使用 NSCopyObject() -(id) copyWithZone:(NSZone*)zone { Foo* clone = [super copyWithZone:zone]; // 创建新的对象 // 必须复制当前子类的实例数据 clone->integer = self->integer; // "integer" 是 int 类型的 // 使用子对象类似的机制复制 clone->objectToClone = [self->objectToClone copyWithZone:zone]; // 有些子对象不能复制,但是可以共享 clone->objectToShare = [self->objectToShare retain]; // 如果有设置方法,也可以使用 [clone setObject:self->object]; return clone; }
NSCopyObject()
NSObject
事实上并没有实现NSCopying
协议(注意函数的原型不同),因此我们不能简单地使用[super copy...]
这样的调用,而是类似[[... alloc] init]
这种标准调用。NSCopyObject()
允许更简单的代码,但是需要注意指针变量(包括对象)。这个函数创建一个对象的二进制格式的拷贝,其原型是:
// extraBytes 通常是 0,可以用于索引实例数据的空间 id NSCopyObject(id anObject, unsigned int extraBytes, NSZone *zone)
二进制复制可以复制非指针对象,但是对于指针对象,需要时刻记住它会创建一个指针所指向的数据的新的引用。通常的做法是在复制完之后重置指针。
// 如果父类没有实现 copyWithZone: -(id) copyWithZone:(NSZone*)zone { Foo* clone = NSCopyObject(self, 0, zone); // 以二进制形式复制数据 // clone->integer = self->integer; // 不需要,因为二进制复制已经实现了 // 需要复制的对象成员必须执行真正的复制 clone->objectToClone = [self->objectToClone copyWithZone:zone]; // 共享子对象必须注册新的引用 [clone->objectToShare retain]; // 设置函数看上去应该调用 clone->object. 但实际上是不正确的, // 因为这是指针值的二进制复制。 // 因此在使用 mutator 前必须重置指针 clone->object = nil; [clone setObject:self->object]; return clone; } // 如果父类实现了 copyWithZone: -(id) copyWithZone:(NSZone*)zone { Foo* clone = [super copyWithZone:zone]; // 父类实现 NSCopyObject() 了吗? // 这对于知道如何继续下面的代码很重要 clone->integer = self->integer; // 仅在 NSCopyObject() 没有使用时调用 // 如果有疑问,一个需要复制的子对象必须真正的复制 clone->objectToClone = [self->objectToClone copyWithZone:zone]; // 不管 NSCopyObject() 是否实现,新的引用必须添加 clone->objectToShare = [self->objectToShare retain]; clone->object = nil; // 如果有疑问,最好重置 [clone setObject:self->object]; return clone; }
Dummy-cloning, mutability,mutableCopy
andmutableCopyWithZone:
如果需要复制不可改变对象,一个基本的优化是假装它被复制了,实际上是返回一个原始对象的引用。从这点上可以区分可变对象与不可变对象。
不可变对象的实例数据不能被修改,只有初始化过程能够给一个合法值。在这种情况下,使用“伪克隆”返回一个原始对象的引用就可以了,因为它本身和它的复制品都不能够被修改。此时,copyWithZone:
的一个比较好的实现是:
-(id) copyWithZone:(NSZone*)zone { // 返回自身,增加一个引用 return [self retain]; }
retain
操作意味着将其引用加 1。我们需要这么做,因为当原始对象被删除时,我们还会持有一个复制品的引用。
“伪克隆”并不是无关紧要的优化。创建一个新的对象需要进行内存分配,相对来说这是一个比较耗时的操作,如果可能的话应该注意避免这种情况。这就是为什么需要区别可变对象和不可变对象。因为不可变对象可以在复制操作上做文章。我们可以首先创建一个不可变类,然后再继承这个类增加可变操作。Cocoa 中很多类都是这么实现的,比如NSMutableString
是NSString
的子类;NSMutableArray
是NSArray
的子类;NSMutableData
是NSData
的子类。
然而根据我们上面描述的内容,似乎无法从不可变对象安全地获取一个完全的克隆,因为不可变对象只能“伪克隆”自己。这个限制大大降低了不可变对象的可用性,因为它们从“真实的世界”隔离了出来。
除了NSCopying
协议,还有一个另外的NSMutableCopying
协议,其原型如下:
-(id) mutableCopyWithZone:(NSZone*)zone;
mutableCopyWithZone:
必须返回一个可变的克隆,其修改不能影响到原始对象。类似NSObject
的copy
函数,也有一个mutableCopy
函数使用默认区块封装了这个操作。mutableCopyWithZone:
的实现类似前面的copyWithZone:
的代码:
// 如果父类没有实现 mutableCopyWithZone: -(id) mutableCopyWithZone:(NSZone*)zone { Foo* clone = [[Foo allocWithZone:zone] init]; // 或者可用 NSCopyObject() clone->integer = self->integer; // 类似 copyWithZone:,有些子对象需要复制,有些需要增加引用 // 可变子对象使用 mutableCopyWithZone: 克隆 //... return clone; }
不要忘记我们可以使用父类的mutableCopyWithZone:
// 如果父类实现了 mutableCopyWithZone: -(id) mutableCopyWithZone:(NSZone*)zone { Foo* clone = [super mutableCopyWithZone:zone]; //... return clone; }