从 C++ 到 Objective-C

Objective-C 可以算作 Apple 平台上“唯一的”开发语言。很多 Objective-C 的教程往往直接从 Objective-C 开始讲起。不过,在我看来,这样做有时候是不合适的。很多程序员往往已经掌握了另外一种开发语言,如果对一门新语言的理解建立在他们已有的知识之上,更能 起到事半功倍的效果。既然名为 Objective-C,它与 C 语言的联系更加密切,然而它又是 Objective 的。与 C 语言联系密切,并且是 Objective 的,我们能够想到的另外一门语言就是 C++。C++ 的开发人员也更普遍,受众也会更多。于是就有了本系列,从 C++ 的角度来讲述 Objective-C 的相关知识。不过,相比 C++,C# 似乎更近一些。不过,我们还是还用 C++ 作为对比。这个系列不会作为一个完整的手册,仅仅是入门。本系列文章不会告诉你 Objective-C 里面的循环怎么写,而是通过与 C++ 的对比来学习 Objective-C 一些更为高级的内容,例如类的实现等等。如果要更好的使用 Objective-C,你需要阅读更多资料。但是,相信在本系列基础之上,你在阅读其他资料时应该会理解的更加透彻一些。

说明:本系列大致翻译来自《From C++ to Objective-C》,你可以在这里找到它的英文 pdf 版本。

下面来简单介绍一下 Objective-C。

要说 Objective-C,首先要从 Smalltalk 说起。Smalltalk 是第一个真正意义上的面向对象语言。Smalltalk 出现之后,很多人都希望能在 C 语言的基础之上增加面向对象的特性。于是就出现了两种新语言:C++ 和 Objective-C。C++ 不必多说,很多人都比较熟悉。Objective-C 则比较冷门。它完全借鉴了 Smalltalk 的思想,有着类似的语法和动态机制;相比来说,C++ 则更加静态一些,目的在于提供能好的性能。Objective-C 最新版本是 2.0.我们的这个系列就是以 Objective-C 2.0 为基础讲解。

Objective-C 是一门语言,而 Cocoa 是这门语言用于 MacOS X 开发的一个类库。它们的关系类似于 C++ 和 Qt,Java 和 Spring 一样。所以,我们完全可以不使用 Cocoa,只去用 Objective-C。例如 gcc 就是一个不使用 Cocoa 的编译器。不过在 MacOS X 平台,几乎所有的功能都要依赖 Cocoa 完成。我们这里只是做一个区别,应该分清 Objective-C 和 Cocoa 的关系。

内存管理

new 和 delete

Objective-C 中没有 new 和 delete 这两个关键字(new 可以看作是一个函数,也就是 alloc+init)。它们实际是被 alloc 和 release 所取代。

引用计数

内存管理是一个语言很重要的部分。在 C 和 C++ 中,内存块有一次分配,并且要有一次释放。这块内存区可以被任意多个指针指向,但只能被其中一个指针释放。Objective-C 则使用引用计数。对象知道自己被引用了多少次,这就像狗和狗链的关系。如果对象是一条狗,每个人都可以拿狗链拴住它。如果有人不想再管它了,只要丢掉他手中的狗链就可以了。只要还有一条狗链,狗就必须在那里;但是只要所有的狗链都没有了,那么此时狗就自由了。换做技术上的术语,新创建的对象的引用计数器被设置为 1。如果代码需要引用这个对象,就可以发送一个 retain 消息,让计数器加 1。当代码不需要的时候则发送一个 release 消息,让计数器减 1。

对象可以接收任意多的 retain 和 release 消息,只要计数器的值是正的。当计数器成 0 时,析构函数 dealloc 将被自动调用。此时再次发送 release 给这个对象就是非法的了,将引发一个内存错误。

这种技术并不同于 C++ STL 的 auto_ptr。Boost 库提供了一个类似的引用计数器,称为 shared_ptr,但这并不是标准库的一部分。

alloc, copy, mutableCopy, retain, release

明白了内存管理机制并不能很好的使用它。这一节的目的就是给出一些使用规则。这里先不解释 autorelease 关键字,因为它比较难理解。

基本规则是,所有使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增加计数器的对象都要用 [auto]release 释放。事实上,有三种方法可以增加引用计数器,也就意味着仅仅有有限种情况下才要使用 release 释放对象:

  • 使用 alloc 显式实例化对象;
  • 使用 copy[WithZone:] 或者 mutableCopy[WithZone:] 复制对象(不管这种克隆是不是伪克隆);
  • 使用 retain

记住,默认情况下,给 nil 发送消息(例如 release)是合法的,不会引起任何后果

autorelease

不一样的 autorelease

前面我们强调了,所有使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增加计数器的对象都要用 [auto]release 释放。事实上,这条规则不仅仅适用于 alloc、retain 和 release。有些函数虽然不是构造函数,但也用于创建对象,例如 C++ 的二元加运算符(obj3 operator+(obj1, obj2))。在 C++ 中,返回值可以在栈上,以便在离开作用域的时候可以自动销毁。但在 Objective-C 中不存在这种对象。函数使用 alloc 分配的对象,直到将其返回栈之前不能释放。下面的代码将解释这种情况:

// 第一个例子 
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2 
{ 
  Point2D* result =[[Point2D alloc] initWithX:([p1 getX] + [p2 getX]) andY:([p1 getY] + [p2 getY])];
  return result; 
}   
// 错误!这个函数使用了 alloc,所以它将对象的引用计数器加 1。 
// 根据前面的说法,它应该被销毁。 
// 但是这将引起内存泄露: 
[calculator add:[calculator add:p1 and:p2]and:p3]
// 第一个算式是匿名的,没有办法 release。所以引起内存泄露。   
// 第二个例子
 -(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2 
{ 
  return [[Point2D alloc]initWithX:([p1 getX] + [p2 getX]) andY:([p1 getY] + [p2 getY])]
} 
// 错误!这段代码实际上和上面的一样, // 不同之处在于仅仅减少了一个中间变量。  
// 第三个例子 
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2 
{ 
  Point2D* result =[[Point2D alloc] initWithX:([p1 getX] + [p2 getX]) andY:([p1 getY] + [p2 getY])];
  [result release]
  return result; 
} 
// 错误!显然,这里仅仅是在对象创建出来之后立即销毁了。

这个问题看起来很棘手。如果没有 autorelease 的确如此。简单地说,给一个对象发送 autorelease 消息意味着告诉它,在“一段时间之后”销毁。但是这里的“一段时间之后”并不意味着“任何时间”。我们将在后面的章节中详细讲述这个问题。现在,我们有了上面这个问题的一种解决方案:

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2 
{ 
  Point2D* result = [[Point2D alloc]initWithX:([p1 getX] + [p2 getX]) andY:([p1 getY] + [p2 getY])]
  [result autorelease];
  return result; // 更简短的代码是:return [result autorelease]; 
} // 正确!result 将在以后自动释放
注意:这里如果使用了autorelease就意味着可以应用于需要返回对象的情况,这样即可以保证返回值可以正常返回又可以保证之后会release掉对象

autorelease 池

上一节中我们了解到 autorelease 的种种神奇之处:它能够在合适的时候自动释放分配的内存。但是如何才能让便以其之道什么时候合适呢?这种情况下,垃圾收集器是最好的选择。下面我们将着重讲解垃圾收集器的工作原理。不过,为了了解垃圾收集器,就不得不深入了解 autorelease 的机制。所以我们要从这里开始。当对象收到 autorelease 消息的时候,它会被注册到一个“autorelease 池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?

答案是丰富多彩的:如果你使用 Cocoa 开发 GUI 界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。

拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用 Cocoa 创建 GUI 程序时,这个 autorelease 池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。这是合乎逻辑的:一般的,一个用户动作都会触发一系列任务,临时变量的创建和销毁一般不会影响到下一个事件。如果必须要有可持久化的数据,那么你就要手动地使用 retain 消息。

另一方面,如果没有 GUI,你必须自己建立 autorelease 池。当对象收到 autorelease 消息时,它能够找到最近的 autorelease 池。当池可以被清空时,你可以对这个池使用 release 消息。一般的,命令行界面的 Cocoa 程序都会有如下的代码:

int main(int argc, char* argv[]) 
  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 
  //... [pool release]; return 0; 
}

注意在 Mac OS X 10.5 的 NSAutoreleasePool 类新增加了一个 drain 方法。这个方法等价于:当垃圾收集器可用时做 release 操作;否则则触发运行垃圾收集。这对编写在两种情况下都适用的代码时是很有用的。注意,这里实际上是说,现在有两种环境:引用计数和垃圾回收。Mac OS 的新版本都会支持垃圾收集器,但是 iOS 却不支持。在引用计数环境下,NSAutoreleasePool 的 release 方法会给池中的所有对象发送 release 消息,如果对象注册了多次,就会多次给它发 release。drain 和 release 在应用计数环境下是等价的。在垃圾收集的环境下,release 不做任何事情,drain 则会触发垃圾收集。

使用多个 autorelease 池

在一个程序中使用多个 autorelease 池也是可以的。对象收到 autorelease 消息时会注册到最近的池。因此,如果一个函数需要创建并使用很大数量临时对象,为了提高性能,可以创建一个局部的 autorelease 池。这种情况下,这些临时变量就可以及时的被销毁,从而在函数返回时就将内存释放出来。

autorelease 的注意点

使用 autorelease 可能会有一些误用情况,需要我们特别注意。

 

  • 首先,非必要地发送多个 autorelease 类似发送多个 release 消息,在内存池清空时会引起内存错误;
  • 其次,即使 release 可以由 autorelease 替代,也不能滥用 autorelease。因为 autorelease 要比正常的 release 消耗资源更多。另外,不必要的推迟 release 操作无疑会导致占用大量内存,容易引起内存泄露。

autorelease 和 retain

多亏了 autorelease,方法才能够创建能够自动释放的对象。但是,长时间持有对象是一种很常见的需求。在这种情形下,我们可以向对象发送 retain 消息,然后在后面手动的 release。这样,这个对象实际上可以从两个角度去看待:

  • 从函数开发者的角度,对象的创建和释放都是有计划的;
  • 从函数调用者的角度,使用了 retain 之后,对象的生命期变长了(使用 retain 将使其引用计数器加 1),为了让对象能够正确地被释放,调用者必须负责将计数器再减 1

我们来理解一下这句话。对于一个函数的开发者,如果他不使用 autorelease,那么,他使用 alloc 创建了一个对象并返回出去,那么,他需要负责在合适的时候对这个对象做 release 操作。也就是说,从函数开发者的角度,这个对象的计数器始终是 1,一次 release 是能够被正常释放的。此时,函数调用者却使用 retain 将计数器加 1,但是开发者不知道对象的计数器已经变成 2 了,一次 release 不能释放对象。所以,调用者必须注意维护计数器,要调用一次 release 将其恢复至 1。

注意:我的理解是如果说持有的对象需要长时间保持,比如说alloc后需要返回,那么就可以使用autorelease

Convenience constructor, virtual constructor

将构造对象的过程分成 alloc 和 init 两个阶段,有时候显得很罗嗦。好在我们有一个 convenience constructor 的概念。这种构造函数应该使用类名做前缀,其行为类似 init,同时要实现 alloc。但是,它的返回对象需要注册到一个内部的 autorelease 池,如果没有给它发送 retain 消息时,这个对象始终是一个临时对象。例如:

// 啰嗦的写法 
NSNumber* zero_a = [[NSNumber alloc] initWithFloat:0.0f]
... [zero_a release]; ... 
// 简洁一些的 
NSNumber* zero_b = [NSNumber numberWithFloat:0.0f]; ... 
// 不需要 release

根据我们前面对内存管理的介绍,这种构造函数的实现是基于 autorelease 的。但是其底层代码并不那么简单,因为这涉及到对 self 的正确使用。事实上,这种构造函数都是类方法,所以 self 指向的是 Class 类型的对象,就是元类类型的。在初始化方法,也就是一个实例方法中,self 指向的是这个类的对象的实例,也就是一个“普通的”对象。

编写错误的这种构造函数是很容易的。例如,我们要创建一个 Vehicle 类,包含一个 color 数据,编写如下的代码:

// The Vehicle class 
@interface Vehicle : NSObject { 
  NSColor* color; 
}   
-(void)setColor:(NSColor*)color;   
// 简洁构造函数 
+(id) vehicleWithColor:(NSColor*)color; 
 @end

其对应的实现是:

// 错误的实现 
+(Vehicle*) vehicleWithColor:(NSColor*)color 
{ 
  // self 不能改变 
  self =[[self alloc] init]
  // 错误! 
  [self setColor:color]
  return [self autorelease]
}

记住我们前面所说的,这里的 self 指向的是 Class 类型的对象。

// 比较正确的实现 
+(id) vehicleWithColor:(NSColor*)color 
{ 
  id newInstance = [[Vehicle alloc] init]
  // 正确,但是忽略了有子类的情况 
  [newInstance setColor:color]
  return[newInstance autorelease]
}

我们来改进一下。Objective-C 中,我们可以实现 virtual constructor。这种构造函数通过内省的机制来了解到自己究竟应该创建哪种类的对象,是这个类本身的还是其子类的。然后它直接创建正确的类的实例。我们可以使用一个 class 方法(注意,class 在 Objective-C 中不是关键字);这是 NSObject 的一个方法,返回当前对象的类对象(也就是 meta-class 对象)。

@implementation Vehicle   
+(id) vehicleWithColor:(NSColor*)color 
{ 
  id newInstance =[[[self class] alloc] init]
  // 完美!我们可以在运行时识别出类 
  [newInstance setColor:color]
  return [newInstance autorelease]
}   
@end  
@interface Car : Vehicle{...}
@end 
... // 创建一个 red Car 
id car = [Car vehicleWithColor:[NSColor redColor]];

类似于初始化函数的 init 前缀,这种简洁构造函数最好使用类名作前缀。不过也有些例外,例如 [NSColor redColor] 返回一个预定义的颜色,按照我们的约定,使用 [NSColor colorRed] 更合适一些。

最后,我们要重复一下,所有使用 alloc、[mutable]copy[WithZone:] 增加引用计数器值的对象,都必须相应地调用 [auto]release。当调用简洁构造函数时,你并没有显式调用 alloc,也就不应该调用 release。但是,在创建这种构造函数时,一定不要忘记使用 autorelease。

Setters

如果不对 Objective-C 的内存管理机制有深刻的理解,是很难写出争取的 setter 的。假设一个类有一个名为 title 的 NSString 类型的属性,我们希望通过 setter 设置其值。这个例子虽然简单,但已经表现出 setter 所带来的主要问题:参数如何使用?不同于 C++,在 Objective-C 中,对象只能用指针引用,因此 setter 虽然只有一种原型,但是却可 以有很多种实现:可以直接指定,可以使用 retain 指定,或者使用 copy。每一种实现都有特定的目的,需要考虑你 set 新的值之后,新值和旧值之间的关系(是否相互影响等)。另外,每一种实现 都要求及时释放旧的资源,以避免内存泄露。直接指定(不完整的代码)

外面传进来的对象仅仅使用引用,不带有 retain。如果外部对象改变了,当前类也会知 道。也就是说,如果外部对象被释放掉,而当前类在使用时没有检查是否为 nil,那么当前类就会持有一个非法引用。

-(void) setString:(NSString*)newString 
{ ... 稍后解释内存方面的细节 
  self->string =newString; 
  // 直接指定 
}

使用 retain 指定(不完整的代码)

外部对象被引用,并且使用 retain 将其引用计数器加 1。外部对象的改变对于当前类也是可见的,不过,外部对象不能被释 放,因为当前类始终持有一个引用。

-(void) setString:(NSString*)newString 
{ ... 稍后解释内存方面的细节 
  self-string =[newString retain]
  // 使用 retain 指定 
}

复制(不完整的代码)

外部对象实际没有被引用,使用的是其克隆。此时,外部对象的改变对于当前类是不可变的。也就是说,当前类持有的是这个对象的克隆, 这个对象的生命周期不会比持有者更长。

-(void) setString:(NSString*)newString 
{ ... 稍后解释内存方面的细节 
  self->string =[newString copy];
  // 克隆 // 使用 NSCopying 协议 
}

为了补充完整这些代码,我们需要考虑这个对象在前一时刻的状态:每一种情形下,setter 都需要释放掉旧的资源,然后建立新的。这些代码看起来比较麻烦。

直接指定( 完整代码)

这是最简单的情况。旧的引用实际上被替换成了新的。

-(void) setString:(NSString*)newString 
{ 
  // 没有强链接,旧值被改变了 
  self->string =newString; 
  // 直接指定 
}

使用 retain 指定(完整代码)

在这种情况下,旧值需要被释放,除非旧值和新值是一样的。

// ------ 不正确的实现 ------ 
-(void) setString:(NSString*)newString 
{ 
  self->string =[newString retain]// 错误!内存泄露,没有引用指向旧的“string”,因此再也无法释放 
}  
-(void) setString:(NSString*)newString 
{ 
  [self->string release]
  self->string =[newString retain]// 错误!如果 newString == string(这是可能的), // newString 引用是 1,那么在 [self->string release] 之后 // 使用 newString 就是非法的,因为此时对象已经被释放 
}  
-(void) setString:(NSString*)newString 
{ 
  if (self->string != newString)
    [self->string release]// 正确:给 nil 发送 release 是安全的 
  self->string = [newString retain]// 错误!应该在 if 里面 // 因为如果 string == newString, // 计数器不会被增加 
}  
// ------ 正确的实现 ------ // 最佳实践:C++ 程序员一般都会“改变前检查” 
-(void)setString:(NSString*)newString 
{ /
  / 仅在必要时修改 
  if (self->string != newString) 
  {
    [self->string release]// 释放旧的 
    self->string = [newString retain]// retain 新的 
   }}   // 最佳实践:自动释放旧值 
-(void) setString:(NSString*)newString 
{ 
  [self->string autorelease]// 即使 string == newString 也没有关系, // 因为 release 是被推迟的
  self->string = [newString retain]//... 因此这个 retain 要在 release 之前发生 
}   
// 最佳实践:先 retain 在 release 
-(void) setString:(NSString*)newString 
{ 
  [self->newString retain]// 引用计数器加 1(除了 nil) 
  [self->string release]// release 时不会是 0
  self->string = newString; // 这里就不应该再加 retain 了 
}

复制(完整代码)

无论是典型的误用还是正确的解决方案,都和前面使用 retain 指定一样,只不过把 retain 换成 copy。

伪克隆

有些克隆是伪克隆,不过对结果没有影响。

Getters

Objective-C 中,所有对象都是动态分配的,使用指针引用。一般的,getter 仅仅返回指针的值,而不应该复制对象。getter 的名字一般和数据成员的名字相同(这一点不同于 Java,JavaBean 规范要求以 get 开头),这并不会引起任何问题。如果是布尔变量,则使用 is 开头(类似 JavaBean 规范),这样可以让程序更具可读性。

@interface Button 
{ 
  NSString* label; 
  BOOL pressed; 
}   
-(NSString*) label; 
-(void)setLabel:(NSString*)newLabel; 
-(BOOL) isPressed; 
@end   
@implementation Button-(NSString*) label 
{ 
  return label; 
}   
-(BOOL) isPressed 
{ 
  return pressed; 
}   
-(void)setLabel:(NSString*)newLabel 
{
  ...
} 
@end

当返回实例数据指针时,外界就可以很轻松地修改其值。这可能是很多 getter 不希望的结果,因为这样一来就破坏了封装性。

@interface Button 
{ 
  NSMutableString* label; 
}   
-(NSString*) label; 
@end  
@implementation Button 
-(NSString*) label 
{ 
  return label; // 正确,但知道内情的用户可以将其强制转换成 NSMutableString, // 从而改变字符串的值 
}   
-(NSString*) label 
{ 
  // 解决方案 1 : 
  return [NSString stringWithString:label]// 正确:实际返回一个新的不可变字符串 
  // 解决方案 2 : 
  return [[label copy] autorelease]// 正确:返回一个不可变克隆,其值是一个 NSString(注意不是 mutableCopy) 
} 
@end

循环 retain

必须紧身避免出现循环 retain。如果对象 A retain 对象 B,B 和 C 相互 retain,那么 B 和 C 就陷入了循环 retain:

A → B ↔ C

如果 A release B,B 不会真正释放,因为 C 依然持有 B。C 也不能被释放,因为 B 持有 C。因为只有 A 能够引用到 B,所以一旦 A release B,就再也没有对象能够引用这个循环,这样就不可避免的造成内存泄露。这就是为什么在一个树结构中,一般是父节点 retain 子节点,而子节点不 retain 父节点。

垃圾收集器

Objective-C 2.0 实现了一个垃圾收集器。换句话说,你可以将所有内存管理交给垃圾收集器,再也不用关心什么 retain、release 之类。但是,不同于 Java,Objective-C 的垃圾收集器是可选的:你可以选择关闭它,从而自己管理对象的生命周期;或者你选择打开,从而减少很多可能有 bug 的代码。垃圾收集器是以一个程序为单位的,因此,打开或者关闭都会影响到整个应用程序。

如果开启垃圾收集器,retain、release 和 autorelease 都被重定义成什么都不做。因此,在没有垃圾收集器情况下编写的代码可以不做任何改变地移植到有垃圾收集器的环境下,理论上只要重新编译一遍就可以了。“理论上”意思是,很多情况下涉及到资源释放处理的时候还是需要特别谨慎地对待。因此,编写同时满足两种情况的代码是不大容易的,一般开发者都会选择重新编写。下面,我们将逐一解释这两者之间的区别,这些都是需要特别注意的地方。

finalize

在有垃圾收集器的环境下,对象的析构顺序是未定义的,因此使用 dealloc 就不大适合了。NSObject 增加了一个 finalize 方法,将析构过程分解为两步:资源释放和有效回收。一个好的 finalize 方法是相当精妙的,需要很好的设计。

weak, strong

很少会见到 __weak 和 __strong 出现在声明中,但我们需要对它们有一定的了解。

默认情况下,一个指针都会使用 __strong 属性,表明这是一个强引用。这意味着,只要引用存在,对象就不能被销毁。这是一种所期望的行为:当所有(强)引用都去除时,对象才能被收集和释放。不过,有时我们却希望禁用这种行为:一些集合类不应该增加其元素的引用,因为这会引起对象无法释放。在这种情况下,我们需要使用弱引用(不用担心,内置的集合类就是这么干的),使用 __weak 关键字。NSHashTable 就是一个例子。当被引用的对象消失时,弱引用会自动设置为 nil。Cocoa 的 Notification Center 就是这么一个例子,虽然这已经超出纯 Objective-C 的语言范畴。

NSMakeCollectable()

Cocoa 并不是 Mac OS X 唯一的 API。Core Foundation 就是另外一个。它们是兼容的,可以共享数据和对象。但是 Core Foudation 是由纯 C 编写的。或许你会认为,Objective-C 的垃圾收集器不能处理 Core Foundation 的指针。但实际上是可以的。感兴趣的话可以关注一下 NSMakeCollectable 的文档。

AutoZone

由 Apple 开发的 Objective-C 垃圾收集器叫做 AutoZone。这是一个公开的开源库,我们可以看到起源代码。不过在 Mac OS X 10.6 中,垃圾收集器可能有了一些变化。这里对此不再赘述。

隐式代码

本章中心是两个能够让代码更简洁的特性。它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;属性则可以让编译器帮我们生成部分代码。键值对编码实际上是 Cocoa 引入的,而属性则是 Objective-C 2.0 语言新增加的。键值对编码(KVC)

原则

键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在 Cocoa 中就是 NSDictionary),数据成员的名字就是这里的键。NSObject 有一个 valueForKey: 和 setValue:forKey: 方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号 . 分割,对应的方法是 valueForKeyPath: 和 setValue:forKeyPath:。

@interface A 
{ 
  NSString* foo; 
} ... // 其它代码 
@end   
@interface B 
{ 
  NSString* bar; 
  A*myA; 
} ... // 其它代码 
@end   
@implementation B ... // 假设 A 类型的对象 a,B 类型的对象 b
  A* a = ...; 
  B* b = ...; 
  NSString* s1 = [a valueForKey:@"foo"]// 正确 
  NSString* s2 =[b valueForKey:@"bar"]// 正确
  NSString* s3 = [b valueForKey:@"myA"]// 正确
  NSString* s4 = [b valueForKeyPath:@"myA.foo"]// 正确 
  NSString* s5 = [b valueForKey:@"myA.foo"]// 错误 
  NSString* s6 = [b valueForKeyPath:@"bar"]// 正确 ...
@end

这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-Value Observing, KVO)等。

拦截

通过 valueForKey: 或者 setValue:forKey: 访问数据不是原子操作。这个操作本质上还是一个方法调用。事实上,这种访问当某些方式实现的情况下才是可用的,例如使用属性自动添加的代码等等,或者显式允许直接访问数据。

Apple 的文档对 valueForKey: 和 setValue:forKey: 的使用有清晰的文档:

对于 valueForKey:@”foo” 的调用:

  • 如果有方法名为 getFoo,则调用 getFoo;
  • 否则,如果有方法名为 foo,则调用 foo(这是对常见的情况);
  • 否则,如果有方法名为 isFoo,则调用 isFoo(主要是布尔值的时候);
  • 否则,如果类的 accessInstanceVariablesDirectly 方法返回 YES,则尝试访问 _foo 数据成员(如果有的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;
  • 如果前一个步骤成功,则返回对应的值;
  • 如果失败,则调用 valueForUndefinedKey:,这个方法的默认实现是抛出一个异常。

对于 forKey:@”foo” 的调用:

  • 如果有方法名为 setFoo:,则调用 setFoo:;
  • 否则,如果类的 accessInstanceVariablesDirectly 返回 YES,则尝试直接写入数据成员 _foo(如果存在的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;
  • 如果失败,则调用 setValue:forUndefinedKey:,其默认实现是抛出一个异常。

注意 valueForKey: 和 setValue:forKey: 的调用可以用于触发任何相关方法。如果没有这个名字的数据成员,则就是一个虚假的调用。例如, 在字符串变量上调用 valueForKey:@”length” 等价于直接调用 length 方法,因为这是 KVC 能够找到的第一个匹配。但是,KVC 的性能不如直接调用方法,所以应当尽量避免。

原型

使用 KVC 有一定的方法原型的要求:getters 不能有参数,并且要返回一个对象;setters 需要有一个对象作为参数,不能有返回值。参数的类型不是很重要的,因为你可以使用 id 作为参数类型。注意,struct 和原生类型(int,float 等)都是支持的:Objective-C 有一个自动装箱机制,可以将这些原生类型封装成 NSNumber 或者 NSValue 对象。因此,valueForKey: 返回值都是一个对象。如果需要向 setValue:forKey: 传入 nil,需要使用 setNilValueForKey:。

高级特性

有几点细节需要注意,尽管在这里并不会很详细地讨论这个问题:

  1. keypath 可以包含计算值,例如求和、求平均、最大值、最小值等;使用 @ 标记;
  2. 注意方法一致性,例如 valueForKey: 或者 setValue:forKey: 以及关联数组集合中常见的 objectForKey: 和 setObject:forKey:。这里,同样使用 @ 进行区分。

属性

使用属性

在定义类时有一个属性的概念。我们使用关键字 @property 来标记一个属性,告诉编译器自动生成访问代码。属性的主要意义在于节省开发代码量。

访问属性的语法比方法调用简单,因此即使我们需要编写代码时,我们也可以使用属性。访问属性同方法调用的性能是一样的,因为属性的使用在编译期实际就是换成了方法调用。大多数时候,属性用于封装成员变量。但是,我们也可以提供一个“假”的属性,看似是访问一个数据成员,但实际不是;换句话说,看起来像是从对象外部调用一个属性,但实际上其实现要比一个值的管理操作要复杂得多。

属性的描述

对属性的描述实际上是要告诉编译器如何生成访问器的代码:

  • 属性从外界是只读的吗?
  • 如果数据成员是原生类型,可选余地不大;如果是对象,那么使用 copy 封装的话,是要用强引用还是弱引用?
  • 属性是线程安全的吗?
  • 访问器的名字是什么?
  • 属性应该关联到哪一个数据成员?
  • 应该自动生成哪一个访问器,哪一个则留给开发人员?

我们需要两个步骤来回答这些问题:

  • 在类的 @interface 块中,属性的声明需要提供附属参数;
  • 在类的 @implementation 块中,访问器可以隐式生成,也可以指定一个实现。

属性访问器是有严格规定的:getter 要求必须返回所期望的类型(或者是相容类型);setter 必须返回 void,并且只能有一个期望类型的参数。访问器的名字也是规定好的:对于数据 foo,getter 的名字是 foo,setter 的名字是 setFoo:。当然,我们也可以指定自定义的名字,但是不同于前面所说的键值对编码,这个名字必须在编译期确定,因为属性的使用被设计成要和方法的直接调用一样的性能。因此,如果类型是不相容的,是不会有装箱机制的。

以下是带有注释的例子,先来有一个大体的了解。

@interface class Car : NSObject 
{ 
  NSString* registration; 
  Person* driver; 
}   // registration 是只读的,使用 copy 设置 
@property NSString* (readonly, copy) registration;   // driver 使用弱引用(没有 retain),可以被修改 
@property Person* (assign) driver;  
@end ... 
@implementation   // 开发者没有提供,由编译期生成 registration 的代码 
@synthesizeregistration;   // 开发者提供了 driver 的 getter/setter 实现 
@dynamic driver;   // 该方法将作为 @dynamic driver 的 getter 
-(Person*) driver { ... }   // 该方法将作为 @dynamic driver 的 setter 
-(void) setDriver:(Person*)value { ... }
@end

属性的参数

属性的声明使用一下模板:

@property type name;

或者

@property(attributes) type name;

如果没有给出属性的参数,那么将使用默认值;否则将使用给出的参数值。这些参数值可以是:

  • readwrite(默认)或者 readonly:设置属性是可读写的(拥有 getter/setter)或是只读的(只有 getter);
  • assign(默认),retain 或 copy:设置属性的存储方式;
  • nonatomic:不生成线程安全的代码,默认是生成的(没有 atomic 关键字);
  • getter=…,setter=…:改变访问器默认的名字。

对于 setter,默认行为是 assign;retain 或者 copy 用于数据成员被修改时的操作。在一个 -(void) setFoo:(Foo*)value 方法中,会因此生成三种不同的语句:

  • self->foo = value ;  // 简单赋值
  • self->foo = [value retain];  // 赋值,同时引用计数器加 1
  • self->foo = [value copy];  // 对象拷贝(必须满足协议 NSCopying)

在有垃圾收集器的环境下,retain 同 assign 没有区别,但是可以加上 __weak 或者 __strong。

@property(copy,getter=getS,setter=setF:) __weak NSString* s; // 复杂声明

注意不要忘记 setter 的冒号 : 。

属性的自定义实现

上一章中我们提到的代码中有两个关键字 @synthesize 和 @dynamic。@dynamic 意思是由开发人员提供相应的代码:对于只读属性需要提供 setter,对于读写属性需要提供 setter 和 getter。@synthesize 意思是,除非开发人员已经做了,否则由编译器生成相应的代码,以满足属性声明。对于上次的例子,如果开发人员提供了 -(NSString*)registration,编译器就会选择这个实现,不会用新的覆盖。因此,我们可以让编译器帮我们生成代码,以简化我们自己的代码输入量。最后,如果编译期没有找到访问器,而且没有使用 @synthesize 声明,那么它就会在运行时添加进来。这同样可以实现属性的访问,但是即使这样,访问器的名字也需要在编译期决定。如果运行期没有找到访问器,就会触发一个异常,但程序不会停止,正如同方法的缺失。当我们使用 @synthesize 时,编译器会被要求绑定某一特定的数据成员,并不一定是一样的名字。

@interface A : NSObject 
{ 
  int _foo; 
} 
@property int foo; 
@end   
@implementation A
@synthesize foo=_foo; // 绑定 "_foo" 而不是 "foo" 
@end

访问属性的语法

为获取或设置属性,我们使用点号:这同简单的 C 结构是一致的,也是在 keypath 中使用的语法,其性能与普通方法调用没有区别。

@interface A : NSObject { int i; } 
@property int i; 
@end   
@interface B : NSObject 
{
  A* myA; 
} 
@property(retain) A* a; 
@end ... 
  A* a = ... 
  B* b = ...;
  a.i = 1// 等价于 [a setI:1]; 
  b.myA.i = 1;// 等价于 [[b myA] setI:1];

请注意上面例子中 A 类的使用。self->i 和 self.i 是有很大区别的:self->i 直接访问数据成员,而 self.i 则是使用属性机制,是一个方法调用。

高级细节

64 位编译器上,Objective-C 运行时环境与 32 位有一些不同。关联到 @property 的实例数据可能被忽略掉,例如被视为隐式的。更多细节请阅读 Apple 的文档。

动态

RTTI (Run-Time Type Information)

RTTI 即运行时类型信息,能够在运行的时候知道需要的类型信息。C++ 有时被认为是一个“假的”面向对象语言。相比 Objective-C,C++ 显得非常静态。这有利于在运行时获得最好的性能。C++ 使用 typeinfo 库提供运行时信息,但这不是安全的,因为这个库依赖于编译器的实现。一般来说,查找对象的类型是一个很少见的请求,因为语言是强类型的,一般在编译时就已经确定其类型了;但是,有时候这种能力对于容器很常用。我们可以使用 dynamic_cast 和 typeid 运算符,但是程序交互则会在一定程度上受限。那么,如何由名字获知这个对象的类型呢?Objective-C 语言可以很容易地实现这种操作。类也是对象,它们继承它们的行为。

class, superclass, isMemberOfClass, isKindOfClass

对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。

isMemberOfClass: 可以用于回答这种问题:“我是给定类(不包括子类)的实例吗?”,而 isKindOfClass: 则是“我是给定类或其子类的实例吗?”使用这种方法需要一个“假”关键字的 class(注意,不是 @class,@class 是用于前向声明的)。事实上,class 是 NSObject 的一个方法,返回一个 Class 对象。这个对象是元类的一个实例。请注意,nil 值的类是 Nil。

BOOL test = [self isKindOfClass:[Foo class]]
if (test) 
  printf("I am an instance of the Foo class\n");

注意,你可以使用 superclass 方法获取其父类。

conformsToProtocol

该方法用于确定一个对象是否和某一协议兼容。我们前面曾经介绍过这个方法。它并不是动态的。编译器仅仅检查每一个显式声明,而不会检查每一个方法。如果一个对象实现了给定协议的所有方法,但并没有显式声明说它实现了该协议,程序运行是正常的,但是 conformsToProtocol: 会返回 NO。

respondsToSelector, instancesRespondToSelector

respondsToSelector: 是一个实例方法,继承自 NSObject。该方法用于检查一个对象是否实现了给定的方法。这里如要使用 @selector。例如:

if ( [self respondsToSelector:@selector(work)] ) 
{ 
  printf("I am not lazy.\n")
  [self work]
}

如果要检查一个对象是否实现了给定的方法,而不检查继承的方法,可以使用类方法 instancesRespondToSelector:。例如:

if ([[self class] instancesRespondToSelector:@selector(findWork)]) 
{ 
  printf("I can find a job without the help of my mother\n")
}

注意,respondsToSelector: 不能用于仅仅使用了前向声明的类。

强类型和弱类型 id

C++ 使用的是强类型:对象必须符合其类型,否则不能通过编译。在 Objective-C 中,这个限制就灵活得多了。如果一个对象与消息的目标对象不相容,编译器仅仅发出一个警告,而程序则继续运行。这个消息会被丢弃(引发一个异常),除非前面已经转发。如果这就是开发人员期望的,这个警告就是冗余的;在这种情形下,使用弱类型的 id 来替代其真实类型就可以消除警告。事实上,任何对象都是 id 类型的,并且可以处理任何消息。这种弱类型在使用代理的时候是必要的:代理对象不需要知道自己被使用了。例如:

-(void) setAssistant:(id)anObject 
{ 
  [assistant autorelease]
  assistant = [anObject retain]
} 
-(void) manageDocument:(Document*)document 
{ 
  if ([assistant respondToSelector:@(manageDocument:)]) 
    [assistant manageDocument:document]
  else
    printf("Did you fill the blue form ?\n")
}

在 Cocoa 中,这种代理被大量用于图形用户界面的设计中。它可以很方便地把控制权由用户对象移交给工作对象。

运行时操作 Objective-C 类

通过添加头文件 <objc/objc-runtime.h>,我们可以调用很多工具函数,用于在运行时获取类信息、添加方法或实例变量。Objective-C 2.0 又引入了一些新函数,比 Objective-C 1.0 更加灵活(例如使用 class_addMethod(…) 替代 class_addMethods(…)),同时废弃了许多 1.0 的函数。这让我们可以很方便的在运行时修改类。


(2011-07-05 22:33:23)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值