从 C++到 Objective-C(7):继承



简单继承

Objective-C 也有继承的概念,但是不能多重继承。不过,它也有别的途径实现类似多重继承的机制,这个我们后面会讲到。

C++

Objective-C

class Foo : public Bar,

          

{

}
@interface Foo : Bar // 单继承

// 如果要同时“继承” Wiz,需要使用另外的技术

{

}

@end

在 C++ 中,一个类可以继承自一个或多个类,使用 public、protected 以及 private 修饰符。子类的函数如果要调用父类的版本,需要使用 :: 运算符,例如 Bar::,Wiz:: 等。

在 Objective-C中,一个类只能继承一个父类,并且只能是 public 的(这和 Java 是一致的)。同样类似 Java,如果你要在子类中调用父类的函数,需要使用 super。

多重继承

protocol 和分类 categories。我们将在后面的内容详细讲述这两种技术。

虚拟性

虚方法

在 Objective-C 中,所有方法都是虚的,因此,没有 virtual 关键字或其等价物。

虚方法重定义

在 Objective-C 中,你可以定义一个没有在 @interface 块里面声明的方法。但这并不是一种替代 private 的机制,因为这种方法实际是能够被调用的(回想下,Objective-C 中方法的调用是在运行期决定的)。不过,这确实能够把接口定义变得稍微干净了一些。

这并不是一种坏习惯,因为有时你不得不重定义父类的函数。由于所有方法都是虚的,你无需像 C++ 一样在声明中显式写明哪些函数是 virtual 的,这种做法就成为一种隐式的重定义。很多继承西 NSObject 的方法都是是用这种方法重定义的。例如构造方法 init,析构方法 dealloc,view 类的 drawRect: 等等。这样的话,接口就变得更简洁,更易于阅读。不好之处就是,你不能知道究竟哪些方法被重定义了。

formal protocols 来实现。

虚继承

Objective-C 中不允许多重继承,因此也就没有虚继承的问题。

协议

Java 和 C# 使用接口 interface 的概念来弥补多重继承的不足。Objective-C 也使用了类似的机制,成为协议 protocol。在 C++ 中,这种概念是使用抽象类。协议并不是真正的类:它只能声明方法,不能添加数据。有两种类型的协议:正式的 formal 和非正式的 informal。

正式协议

正式协议的方法,所有实现这个协议的类都必须实现。这就是一种验证,也就是说,只要这个类说实现这个协议,那么它肯定可以处理协议中规定的方法。一个类可以实现任意多个协议。

C++


class MouseListener
 
  
{
 
  
public:
 
  
  
 
  
  
 
  
};
 
  

 
  
class KeyboardListener
 
  
{
 
  
public:
 
  
  
 
  
};
 
  

 
  
class Foo : public MouseListener, 
 
  
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
 
  
// 然后 Foo 就可以作为鼠标和键盘的事件监听器
 
 
 Objective-C
 
 
@protocol MouseListener
 
  

 
  
-(BOOL) mousePressed;
 
  
-(BOOL) mouseClicked;
 
  

 
  
@end
 
  

 
  
@protocol KeyboardListener
 
  

 
  
-(BOOL) keyPressed;
 
  

 
  
@end
 
  

 
  
@interface Foo : NSObject
 
  
{
 
  
...
 
  
}
 
  
@end
 
  
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
 
  
// 然后 Foo 就可以作为鼠标和键盘的事件监听器


C++ 中,协议可以由抽象类和纯虚函数实现。C++ 的抽象类要比 Objective-C 的协议强大的多,因为抽象类可以带有数据。

Objective-C 中,协议是一个特殊的概念,使用尖括号 <…> 表明。注意,尖括号在 Objective-C 中不是模板的意思,Objective-C 中没有类似 C++ 模板的概念。

一个类也可以不经过协议声明,直接实现协议规定的方法。此时,conformsToProtocol: 方法依然返回 NO。出于性能考虑,conformsToProtocol: 方法只检查类接口的声明,不会一个方法一个方法的对比着检查。conformsToProtocol: 的返回值并不会作为是否调用方法的依据。下面是这个方法的原型:


-(BOOL) conformsToProtocol:(Protocol*)protocol
 
  
// Protocol 对象可以由 @protocol(协议名) 返回



实现了正式协议的对象的类型同协议本身是兼容的。这一机制可以作为协议的筛选操作。例如:



// 下面方法是 Cocoa 提供的标准方法
 
  
// 方法参数可以是任意类型 id,但是必须兼容 NSDraggingInfo 协议
 
  
-(NSDragOperation) draggingEntered:(id )sender;



可选方法

有时我们需要这么一种机制:我们的类需要实现一部分协议中规定的方法,而不是整个协议。例如在 Cocoa 中,代理的概念被广泛使用:一个类可以给定一个辅助类,由这个辅助类去完成部分任务。

一种实现是将一个协议分割成很多小的协议,然后这个类去实现一个协议的集合。不过这并不具有可操作性。更好的解决方案是使用非正式协议。在 Objective-C 1.0 中就有非正式协议了,Objective-C 2.0 则提出了新的关键字 @optional 和 @required,用以区分可选方法和必须方法。


@protocol Slave
 
  

 
  
@required // 必须部分
 
  
-(void) makeCoffee;
 
  
-(void) duplicateDocument:(Document*)document count:(int)count;
 
  

 
  
@optional // 可选部分
 
  
-(void) sweep;
 
  

 
  
@required // 又是一个必须部分
 
  
-(void) bringCoffee;
 
  
@end


非正式协议

非正式协议并不是真正的协议,它对代码没有约束力。非正式协议允许开发者将一些方法进行归类,从而可以更好的组织代码。所以,非正式协议并不是协议的宽松版本。另外一个相似的概念就是分类。

让我们想象一个文档管理的服务。假设有绿色、蓝色和红色三种文档,一个类只能处理蓝色文档,而 Slave 类使用三个协议 manageBlueDocuments, manageGreenDocuments 和 manageRedDocuments。Slave 可以加入一个分类 DocumentsManaging,用来声明它能够完成的任务。分类名在小括号中被指定:


@interface Slave (DocumentsManaging)
 
  

 
  
-(void) manageBlueDocuments:(BlueDocument*)document;
 
  
-(void) trashBlueDocuments:(BlueDocument*)document;
 
  

 
  
@end

任何类都可以加入 DocumentsManaging 分类,加入相关的处理方法:


@interface PremiumSlave (DocumentsManaging)
 
  

 
  
-(void) manageBlueDocuments:(BlueDocument*)document;
 
  
-(void) manageRedDocuments:(RedDocument*)document;
 
  

 
  
@end


另一个开发者就可以浏览源代码,找到了 DocumentsManaging 分类。如果他觉得这个分类中有些方法可能对自己,就会检查究竟哪些能够使用。即便他不查看源代码,也可以在运行时指定:



if([mySlave respondsToSelector:@selector(manageBlueDocuments:)])
 
  
   [mySlave



严格说来,除了原型部分,非正式协议对编译器没有什么意义,因为它并不能约束代码。不过,非正式协议可以形成很好的自解释性代码,让 API 更具可读性。



从 C++到 Objective-C(8):继承(续)



Protocol 对象

运行时,协议就像是类对象,其类型是 Protocol*。例如,conformsToProtocol: 方法就需要接受一个 Protocol* 类型的参数。@protocol 关键字不仅用于声明协议,还可以用于根据协议名返回 Protocol* 对象。



Protocol* myProtocol = @protocol(协议名)



远程对象的消息传递

由于 Objective-C 的动态机制,远程对象之间的消息传递变得很简单。所谓远程对象,是指两个或多个处于不同程序,甚至不同机器,但是可以通过代理完成同一任务,或者交换信息的对象。正式协议就是一种可以确保对象提供了这种服务的有效手段。正式协议还提供了很多额外的关键字,可以更好的说明各种参数。这些关键字分别是 in, out, inout, bycopy, byref 和 oneway。这些关键字仅对远程对象有效,并且仅可以在协议中使用。出了协议,它们就不被认为是关键字。这些关键字被插入到在协议中声明的方法原型之中,提供它们所修饰的参数的额外信息。它们可以告知,哪些是输入参数,哪些是输出参数,哪些使用复制传值,哪些使用引用传值,方法是否是同步的等等。以下是详细说明:

       in:参数是输入参数;

       out:参数是输出参数;

       inout:参数即是输入参数,又是输出参数;

       bycopy:复制传值;

       byref:引用传值;

       oneway:方法是异步的,也就是不会立即返回,因此它的返回值必须是 void。

例如,下面就是一个返回对象的异步方法:



-(oneway void) giveMeAnObjectWhenAvailable:(bycopy out id *)anObject;


默认情况下,参数都被认为是 inout 的。如果参数由 const 修饰,则被当做 in 参数。为参数选定是 in 还是 out,可以作为一种优化手段。参数默认都是传引用的,方法都是同步的(也就是不加 oneway)。对于传值的参数,也就是非指针类型的,out 和 inout 都是没有意义的,只有 in 是正确的选择。

分类

创建类的分类 categories,可以将一个很大的类分割成若干小部分。每个分类都是类的一部分,一个类可以使用任意多个分类,但都不可以添加实例数据。分类的好处是:

       对于精益求精的开发者,分类提供了一种划分方法的机制。对于一个很大的类,它可以将其划分成不同的角色;

       分类允许分开编译,也就是说,同一个类也可以进行多人的分工合作;

       如果把分类的声明放在实现文件(.m)中,那么这个分类就只在文件作用域中可见(虽然这并没有调用上的限制,如果你知道方法原型,依然可以调用)。这样的分类可以取一个合适的名字,比如 FooPrivateAPI;

       一个类可以在不同程序中有不同的扩展,而不需要丢弃通用代码。所有的类都可以被扩展,甚至是 Cocoa 中的类。

最后一点尤其重要。很多开发人员都希望标准类能够提供一些对他们而言很有用的方法。这并不是一个很困难的问题,使用继承即可实现。但是,在单继承的环境下,这会造成出现很多的子类。仅仅为了一个方法就去继承显得有些得不偿失。分类就可以很好的解决这个问题:

C++

Objective-C

class MyString : public string

{

public:

  

  

};



int MyString::vowelCount(void)

{

...

}
@interface NSString (VowelsCounting)

// 注意并没有使用 {}

-(int) vowelCount; // 统计元音的数目

@end



@implementation NSString (VowelsCounting)

-(int) vowelCount

{

...

}

@end

在 C++ 中,这是一个全新的类,可以自由使用。

在 Objective-C 中,NSString 是 Cocoa 框架的一个标准类。它是使用分类机制进行的扩展,只能在当前程序中使用。注意此时并没有新增加类。每一个 NSString 对象都可以从这个扩展获得统计元音数目的能力,甚至常量字符串也可以。同时注意,分类不能增加实例数据,因此没有花括号块。

分类也可以使匿名的,更适合于 private 的实现:


@interface NSString ()
 
  
// 注意并没有使用 {}
 
  
-(int) myPrivateMethod;
 
  
@end
 
  

 
  
@implementation NSString ()
 
  
-(int) myPrivateMethod
 
  
{
 
  
...
 
  
}
 
  
@end



混合使用协议、分类和子类

混合使用协议、分类和子类的唯一限制在于,你不能同时声明子类和分类。不过,你可以使用两步来绕过这一限制:



@interface Foo1 : SuperClass //ok
 
  
@end
 
  

 
  
@interface Foo2 (Category)  //ok
 
  
@end
 
  

 
  
// 下面代码会有编译错误
 
  
@interface Foo3 (Category) 
 
  
@end
 
  

 
  
// 一种解决方案
 
  
@interface Foo3 : SuperClass // 第一步
 
  
@end
 
  

 
  
@interface Foo3 (Category) // 第二步
 
  
@end



从 C++到 Objective-C(9):实例化



类的实例化位导致两个问题:构造函数、析构函数和赋值运算符如何实现,以及如何分配内存。

在 C++ 中,变量默认是“自动的”:除非被声明为 static,否则变量仅在自己的定义块中有意义。动态分配的内存可以一直使用,直到调用了 free() 或者 delete。C++ 中,所有对象都遵循这一规则。

然而在Objective-C 中,所有对象都是动态分配的。其实这也是符合逻辑的,因为 C++ 更加 static,而Objective-C 则更加动态。除非能够在运行时动态分配内存,否则 Objective-C 实现不了这么多动态的特性。

构造函数和初始化函数

分配 allocation和初始化 initialization的区别

在 C++ 中,内存分配和对象初始化都是在构造函数中完成的。在 Objective-C 中,这是两个不同的函数。

内存分配由类方法 alloc 完成,此时将初始化所有的实例数据。实例数据将被初始化为 0,除了一个名为 isa 的 NSObject 的指针。这个指针将在运行时指向对象的实际类型。实例数据根据传入的参数初始化为某一特定的值,这一过程将在一个实例方法 instance method 中完成。这个方法通常命名为 init。因此,构造过程被明确地分为两步:内存分配和初始化。alloc 消息被发送给类,而 init 消息则被发送给由 alloc 创建出来的新的对象。初始化过程不是可选的,alloc 之后应该跟着 init,之后,父类的 init 也会被调用,直到 NSObject 的 init 方法。这一方法完成了很多重要的工作。

在 C++ 中,构造函数的名字是规定好的,必须与类名一致。在 Objective-C 中,初始化方法与普通方法没有什么区别。你可以用任何名字,只不过通常都是选用 init 这个名字。然而,我们还是强烈建议,初始化方法名字一定要用 init 或者 init 开头的字符串

使用 alloc 和 init

调用 alloc 之后将返回一个新的对象,并且应该给这个对象发送一个 init 消息。init 调用之后也会返回一个对象。通常,这就是初始化完成的对象。有时候,如果使用单例模式,init 可能会返回另外的对象(单例模式要求始终返回同一对象)。因此,init 的返回值不应该被忽略。通常,alloc 和 init 都会在一行上。

C++
 
 
Foo* foo = new Foo;
 
 
 Objective-C
 
 
Foo* foo1 = [Foo alloc];
 
  
[foo1 init]; // 这是不好的行为:应该使用 init 的返回值
 
  
Foo* foo2 = [Foo alloc];
 
  
foo2 = [foo2 init]; // 正确,不过看上去很啰嗦
 
  
Foo* foo3 = [[Foo alloc] init]; // 正确,这才是通常的做法



初始化方法的正确示例代码

一个正确的初始化方法应该有如下特点:

       名字以init 开始;

       返回能够使用的对象;

       调用父类的 init 方法,直到 NSObject 的init 方法被调用;

       保存[super init...] 的返回值;

       处理构造期间出现的任何错误,无论是自己的还是父类的。

下面是一些代码:

C++
 
 
class Point2D
 
  
{
 
  
public:
 
  
  
 
  
private:
 
  
  
 
  
   int y;
 
  
};
 
  
Point2D::Point2D(int anX, int anY) {x = anX; y = anY;}
 
  
...
 
  

 
  
Point2D 
 
  
Point2D* p2 = new Point2D(5, 6);
 
 
 Objective-C
 
 
@interface Point2D : NSObject
 
  
{
 
  
  
 
  
  
 
  
}
 
  

 
  
// 注意,在 Objective-C 中,id 类似于 void*
 
  
// (id) 就是对象的“一般”类型
 
  
-(id) initWithX:(int)anX andY:(int)anY;
 
  
@end
 
  

 
  
@implementation Point2D
 
  

 
  
-(id) initWithX:(int)anX andY:(int)anY
 
  
{
 
  
  // 调用父类的初始化方法
 
  
   if (!(self = [super init])) // 如果父类是 NSObject,必须进行 init 操作
 
  
       return nil; // 如果父类 init 失败,返回 nil
 
  
  // 父类调用成功,进行自己的初始化操作
 
  
  self->x = anX;
 
  
  
 
  
   return self; // 返回指向自己的指针
 
  
}
 
  
@end
 
  

 
  
...
 
  
Point2D* p1 = [[Point2D alloc] initWithX:3 andY:4];



从 C++到 Objective-C(10):实例化(续)



self = [super init...]

在上一篇提到的代码中,最不可思议的可能就是这句 self = [super init...]。回想一下,self 是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self 必须要改变。我们将在下面解释为什么要这样做。

[super init] 实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而, 有一个 API 可以用一个对象替换新分配的对象。Core Data(Apple 提供的 Cocoa 里面的一个 API)就是用了这种 API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承 NSManagedObject 类的时候,就需要仔细对待这种替换。在这种情形下,self 就要指向两个对象:一个是 alloc 返回的对象,一个是 [super init] 返回的对象。修改 self 的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:



@interface B : A
{
int i;
}

@end

@implementation B

-(id) init
{
   // 此时,self 指向 alloc 返回的值
   // 假设 A 进行了替换操作,返回一个不同的 self
  
   NSLog(@"%d", i); // 输出 self->i 的值
   self = newSelf; // 有人会认为 i 没有变化
   NSLog(@"%d", i); // 事实上,此时的 self->i, 实际是 newSelf->i,
                    // 和之前的值可能不一样了
  
}

@end
...
B* b = [[B alloc] init];



self = [super init] 简洁明了,也不必担心以后会引入 bug。然而,我们应该注意旧的 self 指向的对象的命运:它必须被释放。第一规则很简单:谁替换 self 指针,谁就要负责处理旧的 self 指针。在这里,也就是 [super init] 负责完成这一操作。例如,如果你创建 NSManagedObject 子类(这个类会执行替换操作),你就不必担心旧的 self 指针。事实上,NSManagedObject 的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?

初始化错误

初始化出错可能发生在三个地方:

   调用 [super init...] 之前:如果构造函数参数非法,那么初始化应该立即停止;

   调用 [super init...] 期间:如果父类调用失败,那么当前的初始化操作也应该停止;

   调用 [super init...] 之后:例如资源分配失败等。

在上面每一种情形中,只要失败,就应该返回 nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1, 3情况。要释放当前对象,我们调用 [self release] 即可。

在调用 dealloc 之后,对象的析构才算完成。因此,dealloc 的实现必须同初始化方法兼容。事实上,alloc 将所有的实例数据初始化成 0 是相当有用的。


@interface A : NSObject {
  
}

-(id) initWithN:(unsigned int)value;
@end

@implementation A

-(id) initWithN:(unsigned int)value
{
   // 第一种情况:参数合法吗?
   if (value == 0) // 我们需要一个正值
  
      
      
  
   // 第二种情况:父类调用成功吗?
   if (!(self = [super init])) // 即是 self 被替换,它也是父类
       return nil; // 错误发生时,谁负责释放 self?
   // 第三种情况:初始化能够完成吗?
  
   void* p = malloc(n); // 尝试分配资源
   if (!p) // 如果分配失败,我们希望发生错误
  
      
      
  
}
@end


将构造过程合并为 alloc+init

有时候,alloc 和 init 被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到 Objective-C 的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似 init,但要自己实现 alloc。然而,这个对象需要注册到 autorelease 池中,除非发送 retain 消息,否则其生命周期是有限制的。以下即是示例代码:


// 啰嗦的写法
NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0.0f];
...
[tmp1 release];
// 简洁一些
NSNumber* tmp2 = [NSNumber numberWithFloat:0.0f];
...
// 无需调用 release



从 C++到 Objective-C(11):实例化(续二)



默认构造函数:指定初始化函数

在 Objective-C 中,默认构造函数没有实在的意义,因为所有对象都是动态分配内存,也就是说,构造函数都是确定的。但是,一个常用的构造函数确实可以精简代码。事实上,一个正确的初始化过程通常类似于:


if (!(self = [super init])) // "init" 或其他父类恰当的函数
 
  
  
 
  
// 父类初始化成功,继续其他操作……
 
  
return self;

剪贴复制代码是一个不良习惯。好的做法是,将共同代码放到一个独立的函数中,通常称为“指定初始化函数”。通常这种指定初始化函数会包含很多参数,因为 Objective-C 不允许参数有默认值。


-(id) initWithX:(int)x
 
  
{
 
  
  
 
  
}
 
  

 
  
-(id) initWithX:(int)x andY:(int)y
 
  
{
 
  
  
 
  
}
 
  

 
  
// 指定初始化函数
 
  
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
  
 
  
  
 
  
}


如果指定初始化函数没有最大数量的参数,那基本上就没什么用处:

// 以下代码就有很多重复部分
 
  
-(id) initWithX:(int)x // 指定初始化函数
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
}
 
  

 
  
-(id) initWithX:(int)x andY:(int)y
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
}
 
  

 
  
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
  
 
  
}

初始化列表和实例数据的默认值

Objective-C 中不存在 C++ 构造函数的初始化列表的概念。然而,不同于 C++,Objective-C的 alloc 会将所有实例数据初始化成 0,因此指针也会被初始化成 nil。C++ 中,对象属性不同于指针,但是在 Objective-C 中,所有对象都被当做指针处理。

虚构造函数

Objective-C 中存在虚构造函数。我们将在后面的章节中详细讲诉这个问题。

类构造函数

在 Objective-C 中,类本身就是对象,因此它也有自己的构造函数,并且也能够被重定义。它显然是一个类函数,继承自 NSObject,其原型是 +(void) initialize;。

第一次使用这个类或其子类的时候,这个函数将被自动调用。但这并不意味着,对于指定的类,这个函数只被调用一次。事实上,如果子类没有定义 +(void) initialize;,那么 Objective-C 将调用其父类的 +(void) initialize;。

析构函数

在 C++ 中,析构函数同构造函数一样,是一个特殊的函数。在 Objective-C 中,析构函数也是一个普通的实例函数,叫做 dealloc。C++ 中,当对象被释放时,析构函数将自动调用;Objective-C 也是类似的,但是释放对象的方式有所不同。

析构函数永远不应该被显式调用。在 C++ 中存在这么一种情况:开发者自己在析构时管理内存池。但是在 Objective-C 中没有这种限制。你可以在 Cocoa 中使用自定义的内存区域,但是这并不会影响平常的内存的分配、释放机制。

C++
 
 
class Point2D
 
  
{
 
  
public:
 
  
  
 
  
};
 
  

 
  
Point2D::~Point2D() {}
 
 
 Objective-C
 
 
@interface Point2D : NSObject
 
  
-(void) dealloc; // 该方法可以被重定义
 
  
@end
 
  

 
  
@implementation Point2D
 
  
// 在这个例子中,重定义并不需要
 
  
-(void) dealloc
 
  
{
 
  
   [super dealloc]; // 不要忘记调用父类代码
 
  
}
 
  
@end


从 C++到 Objective-C(12):实例化(续三)



复制运算符

典型 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
 
  
{
 
  
   // 创建对象
 
  
  
 
  
   // 实例数据必须手动复制
 
  
   clone->integer = self->integer; // "integer" 是 int 类型的
 
  
   // 使用子对象类似的机制复制
 
  
  
 
  
   // 有些子对象不能复制,但是可以共享
 
  
  
 
  
   // 如果有设置方法,也可以使用
 
  
   [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 setObject:self->object];
 
  
   return clone;
 
  
}



NSCopyObject()

NSObject 事实上并没有实现 NSCopying 协议(注意函数的原型不同),因此我们不能简单地使用 [super copy...] 这样的调用,而是类似 [[... alloc] init] 这种标准调用。NSCopyObject() 允许更简单的代码,但是需要注意指针变量(包括对象)。这个函数创建一个对象的二进制格式的拷贝,其原型是:


// extraBytes 通常是 0,可以用于索引实例数据的空间
 
  
id


二进制复制可以复制非指针对象,但是对于指针对象,需要时刻记住它会创建一个指针所指向的数据的新的引用。通常的做法是在复制完之后重置指针。


// 如果父类没有实现 copyWithZone:
 
  
-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
   Foo* clone = NSCopyObject(self, 0, zone); // 以二进制形式复制数据
 
  
   // clone->integer = self->integer; // 不需要,因为二进制复制已经实现了
 
  
   // 需要复制的对象成员必须执行真正的复制
 
  
  
 
  
   // 共享子对象必须注册新的引用
 
  
   [clone->objectToShare retain];
 
  
   // 设置函数看上去应该调用 clone->object. 但实际上是不正确的,
 
  
   // 因为这是指针值的二进制复制。
 
  
   // 因此在使用 mutator 前必须重置指针
 
  
  
 
  
   [clone setObject:self->object];
 
  
   return clone;
 
  
}
 
  

 
  
// 如果父类实现了 copyWithZone:
 
  
-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
  
 
  
   // 父类实现 NSCopyObject() 了吗?
 
  
   // 这对于知道如何继续下面的代码很重要
 
  
   clone->integer = self->integer; // 仅在 NSCopyObject() 没有使用时调用
 
  
   // 如果有疑问,一个需要复制的子对象必须真正的复制
 
  
  
 
  
   // 不管 NSCopyObject() 是否实现,新的引用必须添加
 
  
  
 
  
   clone->object = nil; // 如果有疑问,最好重置
 
  
   [clone setObject:self->object];
 
  
   return clone;
 
  
}



Dummy-cloning,mutability, mutableCopy and mutableCopyWithZone:

如果需要复制不可改变对象,一个基本的优化是假装它被复制了,实际上是返回一个原始对象的引用。从这点上可以区分可变对象与不可变对象。

不可变对象的实例数据不能被修改,只有初始化过程能够给一个合法值。在这种情况下,使用“伪克隆”返回一个原始对象的引用就可以了,因为它本身和它的复制品都不能够被修改。此时,copyWithZone: 的一个比较好的实现是:


-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
   // 返回自身,增加一个引用
 
  
   return [self retain];
 
  
}

retain 操作意味着将其引用加 1。我们需要这么做,因为当原始对象被删除时,我们还会持有一个复制品的引用。

“伪克隆”并不是无关紧要的优化。创建一个新的对象需要进行内存分配,相对来说这是一个比较耗时的操作,如果可能的话应该注意避免这种情况。这就是为什么需要区别可变对象和不可变对象。因为不可变对象可以在复制操作上做文章。我们可以首先创建一个不可变类,然后再继承这个类增加可变操作。Cocoa 中很多类都是这么实现的,比如 NSMutableString 是 NSString 的子类;NSMutableArray是 NSArray 的子类;NSMutableData 是 NSData 的子类。

然而根据我们上面描述的内容,似乎无法从不可变对象安全地获取一个完全的克隆,因为不可变对象只能“伪克隆”自己。这个限制大大降低了不可变对象的可用性,因为它们从“真实的世界”隔离了出来。

除了 NSCopy 协议,还有一个另外的 NSMutableCopying 协议,其原型如下:


-(id) mutableCopyWithZone:(NSZone*)zone;



mutableCopyWithZone: 必须返回一个可变的克隆,其修改不能影响到原始对象。类似 NSObject 的 copy 函数,也有一个mutableCopy 函数使用默认区块封装了这个操作。mutableCopyWithZone:的实现类似前面的 copyWithZone: 的代码:


// 如果父类没有实现 mutableCopyWithZone:
 
  
-(id) mutableCopyWithZone:(NSZone*)zone
 
  
{
 
  
   Foo* clone = [[Foo allocWithZone:zone] init]; // 或者可用 NSCopyObject()
 
  
  
 
  
   // 类似 copyWithZone:,有些子对象需要复制,有些需要增加引用
 
  
   // 可变子对象使用 mutableCopyWithZone: 克隆
 
  
   //...
 
  
   return clone;
 
  
}
 
 
 不要忘记我们可以使用父类的 mutableCopyWithZone:
 
 
// 如果父类实现了 mutableCopyWithZone:
 
  
-(id) mutableCopyWithZone:(NSZone*)zone
 
  
{
 
  
  
 
  
   //...
 
  
   return clone;
 
  
}