iOS开发读书笔记:Effective Objective-C 2.0 52个有效方法 - 篇1/4

iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇1/4
iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇2/4
iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇3/4
iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇4/4

  • 第一章 熟悉Objective-C
    • 第1条:了解Objective-C语言的起源
    • 第2条:在类的头文件中尽量少引入其他头文件
    • 第3条:多用字面量语法,少用阈值等价的方法
    • 第4条:多用类型常量,少用#define预处理指令
    • 第5条:用枚举表示状态、选项、状态码
  • 第二章 对象、消息、运行期
    • 第6条:理解“属性”这一概念
    • 第7条:在对象内部尽量直接访问实例变量
    • 第8条:理解“对象等同性”这一概念
    • 第9条:以“类族模式”隐藏实现细节
    • 第10条:在既有类中使用关联对象存放自定义数据

第一章 熟悉Objective-C

Objective-C是在C语言基础上添加了面向对象特性。

第1条:了解Objective-C语言的起源

Objective-C与C++、Java等面向对象语言类似,有所差别是因为该语言使用“消息结构”(messaging structure)而非“函数调用”(Unction calling)。消息与函数调用之间的区别看上去就像这样:

// Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];

// Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1,parameter2);

关键区别在于:

  • 使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;
  • 使用函数调用的语言,则由编译器决定;

如果范例代码中调用的函数是多态的,那么在运行时就要按照“虚方法表”(virtual table是编程语言为实现“动态派发”(dynamic dispatch)或“运行时方法绑定”(runtime method binding)而采用的一种机制)来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去査找所要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,其过程叫做“动态绑定"(dynamic binding)。

Objective-C的重要工作都由“运行期组件”(runtime component)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说, 运行期组件中含有全部内存管理方法。运行期组件本质上就是一种与开发者所编代码相链接的“动态库”(dynamic library),其代码能把开发者编写的所有程序粘合起来。这样的话,只需更新运行期组件,即可提升应用程序性能。而那种许多工作都在“编译期”(compile time) 完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。

Objective-C是C的“超集”(superset),所以C语言中的所有功能在编写Objective-C代码时依然适用。其中尤为重要的是要理解C语言的内存模型(memory model),这有助于理解Objective-C的内存模型及其“引用计数”(reference counting)机制的工作原理。 若要理解内存模型,则需明白:Objective-C语言中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用如下语法:

NSString *someString = @"The string";

这种语法基本上是照搬C语言的,它声明了一个名为someString的变量,其类型是NSString *。也就是说,此变量为指向NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈” (stack)上。
如果在栈中分配Objective-C对象,编译报错:

NSString stackstring;
//error: interface type cannot be statically allocated

someString变量指向分配在堆里的某块内存,其中含有一个NSString对象。也就是说, 如果再创建一个变量,令其指向同一地址,那么并不拷贝该对象,只是这两个变量会同时指向此对象:

NSString *someString = @"The string";
NSString *anotherString = someString;

只有一个NSString实例,然而有两个变量指向此实例。两个变量都是NSString *型,这说明当前“栈帧”(stack frame)里分配了两块内存,每块内存的大小都能容下一枚指针(在32位架构的计算机上是4字节,64位计算机上是8字节)。这两块内存里的值都一样,就是NSString实例的内存地址。
下图描述了此时的内存布局。存放在NSString实例中的数据含有代表字符串实际内容的字节。

内存布局.png

分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。

Objective-C将堆内存管理抽象出来了。不需要用malloc及free来分配或释放对象所占内存。Objective-C运行期环境把这部分工作抽象为一套内存管理架构,名叫“引用计数”。

在Objective-C代码中,有时会遇到定义里不含*的变量,它们可能会使用“栈空间” (stack space)。这些变量所保存的不是Objective-C对象。比如Core Graphics框架中的CGRect就是个例子:

CGRect frame;
frame.origin.x = 0.0f;

CGRect是C结构体,其定义是:

struct CGRect {
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;

整个系统框架都在使用这种结构体,因为如果改用Objective-C对象来做的话,性能会受影响。与创建结构体相比,创建对象还需要额外开销,例如分配及释放堆内存等。如果只需保存int、float、double、char等“非对象类型”(nonobject type),那么通常使用CGRect这种结构体就可以了。

要点:

  1. Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
  2. 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型与指针。

第2条:在类的头文件中尽量少引入其他头文件

与C和C++一样,Objective-C也使用“头文件”(header file)与“实现文件” (implementation file)来区隔代码。用Objective-C语言编写“类”(class)的标准方式为:以类名做文件名,分别创建两个文件,头文件后缀用.h,实现文件后缀用.m。

创建连个类:EOCPerson和EOCEmployer。为EOCPerson类添加一项属性,在EOCPerson.h中加入下面这行:

@property (nonatomic, strong) EOCEmployer *employer; 

在编译一个使用了EOCPerson类的文件时,不需要知道 EOCEmployer类的全部细节,只需要知道有一个类名叫EOCEmployer就好。可以使用“向前声明"(forward declaring)该类能把这一情况告诉编译器:

@class EOCEmployer;

EOCPerson类的实现文件则需引入EOCEmployer类的头文件,因为若要使用后者,则必须知道其所有接口细节。

将引入头文件的时机尽量延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入的头文件数量,减少编译时间。

向前声明也解决了两个类互相引用的问题。此时,若要编译EOCEmployer,则编译器必须知道EOCPerson这个类,而要编译EOCPerson,则又必须知道EOCEmployer。如果在各自头文件中引入对方的头文件,则会导致“循环引用”(chicken-and-egg situation)。当解析其中一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件。使用#import而非#include指令虽然不会导致死循环,但却这意味着两个类里有一个无法被正确编译。

但是有时候必须要在头文件中引入其他头文件:

  1. 如果你写的类继承自某个超类,则必须引入定义那个超类的头文件;
  2. 同理,如果要声明你写的类遵从某个协议(protocol),那么该协议必须有完整定义,且不能使用向前声明。向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。鉴于此,最好是把协议单独放在一个头文件中,那么只要引入此协议,就必定会引入那个头文件中的全部内容。

然而有些协议,例如“委托协议”(delegate protocol),就不用单独写一个头文件了。在那种情况下,协议只有与接受协议委托的类放在一起定义才有意义。此时最好能在实现文件中声明此类实现了该委托协议。

要点:

  1. 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
  2. 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-cominuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

第3条:多用字面量语法,少用与之等价的方法

Objective-C以语法繁杂而著称。不过,从Objective-C 1.0起,有一种非常简单的方式能创建NSString对象。这就是“字符串字面量” (string literal),其语法如下:

NSString *someString = @"Effective Objective-C 2.0";

如果不用这种语法的话,就要以常见的alloc及init方法来分配并初始化NSString对象了。

字面数值

有时需要把整数、浮点数、布尔值封入Objective-C对象中。这种情况下可以用NSNumber类,该类可处理多种类型的数值。

字面量数组

用字面量语法创建数组时要注意,若数组元素对象中有nil,则会抛出异常,因为字面量语法实际上只是一种“语法糖"(syntactic sugar:也称“糖衣语法”,是指计算机语言中与另外一套语法等效但是开发者用起来却更加方便的语法。语法糖 可令程序更易读,减少代码出错机率。),其效果等于是先创建了一个数组,然后把方括号内的所有对象都加到这个数组中。

在改用字面量语法来创建数组时就会遇到这个问题。下面这段代码分别以两种语法创建数组:

id object1 = /* ... */;
id object2 = /* ... */;
id object3 = /* ... */;

NSArray *arrayA = [NSArray array WithObjects:object1,object2, object3, nil];
NSArray *arrayB « @[object1,object2,object3];

如果object1与object3都指向了有效的Objective-C对象,而object2是nil, 那么会出现什么情况呢?按字面量语法创建数组arrayB时会抛出异常。arrayA虽然能创建出来,但是其中却只含有object1 一个对象。原因在于,“arrayWithObjects:”方法会依次处理各个参数,直到发现nil为止,由于object2是nil,所以该方法会提前结束。

这个微妙的差别表明,使用字面量语法更为安全。抛出异常令应用程序终止执行,这比创建好数组之后才发现元素个数少了要好。向数组中插入nil通常说明程序有错,而通过异常可以更快地发现这个错误。

字面量字典

“字典”(Dictionary)是一种映射型数据结构,可向其中添加键值对,字典中的对象和键必须都是Objective-C对象。

与数组一样,用字面量语法创建字典时也有个问题,那就是一旦有值为nil,便会拋出异常。不过基于同样的原因,这也是个好事。假如在创建字典时不小心用了空值对象,那 么“dictionaryWithObjectsAndKeys:”方法就会在首个nil之前停下,并抛出异常,这有助于査错。

可变数组与字典

如果数组与字典对象是可变的(mutable),那么也能通过下标修改其中的元素值。修改可变数组与字典内容的标准做法是:

[mutableArray replaceObjectAtlndex: 1 withObject :@"dog"];
[mutableDictionary setObject: @"Galloway" forKey: @"lastName"];

若换用取下标操作来写,则是:

mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";

局限性

字面量语法有个小小的限制,就是除了字符串以外,所创建出来的对象必须属于Foundation框架才行。如果自定义了这些类的子类,则无法用字面量语法创建其对象。很少有人会从其中自定义子类。

使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,则需复制一份。这么做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多于上述缺点的。

要点:

  1. 应该使用字面虽语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
  2. 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
  3. 用字面童语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。

第4条:多用类型常量,少用#define预处理指令

用宏定义来定义一个常量:

#define ANIMATION_DURATION 0.3

上述预处理指令会把源代码中的ANIMATION_DURATION字符串替换为0.3,有如下缺点:

  1. 定义出来的常量没有类型信息;
  2. 预处理过程会把碰到的所有ANIMATION_DURATION一律替换成0.3,这样的话,假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。

要想解决此问题,应该设法利用编译器的某些特性才对。有个办法比用预处理指令来定义常量更好。下面就定义了一个类型为NSTimelnterval的常量:(请注意,用此方式定义的常量包含类型信息,其好处是清楚地描述了常量的含义。)

static const NSTimelnterval kAnimationDuration = 0.3;

注意常量名称:常用的命名法是:若常量局限于某“编译单元"(translation unit,也就是“实现文件”,implementation file)之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。

注意定义常量的位置:我们总喜欢在头文件里声明预处理指令,这样做真的很糟糕,当常量名称有可能互相冲突时更是如此。例如,ANIMATION_DURATION这个常量名就不该用在头文件中,因为所有引入了这份头文件的其他文件中都会出现这个名字。其实就连用static const定义的那个常量也不应出现在头文件里。因为Objective-C没有“名称空间” (namespace)这一概念,所以那样做等于声明了一个名叫kAnimationDuration的全局变量。 此名称应该加上前缀,以表明其所属的类,例如可改为EOCViewClassAnimationDuration。

若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。

变量一定要同时用static与const来声明:如果试图修改由const修饰符所声明的变量, 那么编译器就会报错。而static修饰符则意味着该变量仅在定义此变量的编译单元中可见。编译器每收到一个编译单元,就会输出一份“目标文件”(object file)。在Objective-C的语境下,“编译单元”一词通常指每个类的实现文件(以.m为后缀名)。因此,在上述代码中声明的kAnimationDuration变量,其作用域仅限于由EOCAnimatedView.m所生成的目标文件中。假如声明此变量时不加static,则编译器会为它创建一个“外部符号”(external symbol)。此时若是另一个编译单元中也声明了同名变量,那么编译器就拋出一条错误消息:

duplicate symbol _kAnimationDuration in:
    EOCAnimatedView.o 
    EOCOtherView.o

实际上,如果一个变量既声明为static,又声明为const,那么编译器根本不会创建符号, 而是会像执行预处理指令一样,把所有遇到的变量都替换为常值。不过还是要记住:用这种方式定义的常量带有类型信息。

有时候需要对外公开某个常值变量(constant variable)。此类常量需放在“全局符号表”(global symbol table)中,以便可以在定义该常量的编译单元之外使用。因此,其定义方式与上例演示的static const有所不同。应该这样来定义:

//in the header file
extern NSString *const EOCStringConstant;

// In the implementation file
NSString *const EOCStringConstant = @"VALUE";

这个常量在头文件中“声明”,且在实现文件中“定义”。注意const修饰符在常量类型中的位置。常量定义应从右至左解读,所以在本例中,EOCStringConstant就是“一个常量, 而这个常量是指针,指向NSString对象”。这与需求相符:我们不希望有人改变此指针常量, 使其指向另一个NSString对象。

编译器看到头文件中的extern关键字,就能明白如何在引入此头文件的代码中处理该常量了。这个关键字是要告诉编译器,在全局符号表中将会有一个名叫EOCStringConstant的符号。也就是说,编译器无须查看其定义,即允许代码使用此常量。因为它知道,当链接成二进制文件之后,肯定能找到这个常量。

此类常量必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件里。由实现文件生成目标文件时,编译器会在“数据段”(data section)为字符串分配存储空间。链接器会把此目标文件与其他目标文件相链接,以生成最终的二进制文件。 凡是用到EOCStringConstant这个全局符号的地方,链接器都能将其解析。

因为符号要放在全局符号表里,所以命名常量时需谨慎。为避免名称冲突,最好是用与之相关的类名做前缀。系统框架中一般都这样做。例如UIKit就按照这种方式来声明用作通知名称的全局常量。其中有类似UIApplicationDidEnterBackgroundNotification 与UIApplicationWillEnterForegroundNotification 这样的常量名。 其他类型的常量也是如此。

要点:

  1. 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
  2. 在实现文件中使用static const来定义“只在编译单元内可见的常量”(translation-unit- specific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
  3. 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。

第5条:用枚举表示状态、选项、状态码

由于Objective-C基于C语言,所以C语言有的功能它都有。其中之一就是枚举类型:enum

枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)。

除了正常使用枚举之外,还有一种情况应该使用枚举类型,那就是定义选项且选项可以彼此组合。只要枚举定义的对,各选项之间就可通过“按位或操作符”(bitwise OR operator)来组合。

每个枚举值(UIVievvAutoresizingNone除外,它的值是0,对应的二进制值也是0,其中没有值为1的二进制位)所对应的二进制表示中,只有1个二进制位的值是1。用‘按位或操作符’可组合多个选项。下图中列出了每个枚举成员的二进制值,并演示了其中两个枚举组合之后的值。


枚举.png
enum UIViewAutoresizing resizing = UIViewAutoresizingFlexiblGWidth | UIViewAutoresizingFlexibleHeight; 
if (resizing & UIViewAutoresizingFlexibleWidth) { 
    // UIViewAutoresizingFlexibleWidth is set
};

要点:

  1. 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
  2. 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
  3. 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,可以指定用于保存枚举值的底层数据类型。
  4. 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后, 编译器就会提示开发者:switch语句并未处理所有枚举。

第二章 对象、消息、运行期

用Objective-C等面向对象语言编程时,“对象”(object)就是“基本构造单元"(building block),开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程叫做“消息传递”(Messaging)。若想编写出髙效且易维护的代码,就一定要熟悉这两个特性的工作原理。

当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C运行期环境” (Objective-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。

第6条:理解“属性”这一概念

“属性”(property)用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法"(getter)用于读取变量值,而设置方法(setter)用于写入变量值。 开发者可以令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”(dot syntax),使开发者可以更为容易地依照类对象来访问存放于其中的数据。

在描述个人信息的类中,也许会存放人名等内容。可以在类接口的public区段中声明一些实例变量:

@interface EOCPerson : NSObject {

  @public
  NSString *_firstName;                            .
  NSString *_lastName;

  @private
  NSString *_someInternalData;
}
@end

原来编过Java或C++程序的人应该比较熟悉这种写法,在这些语言中,可以定义实例变量的作用域。然而编写Objective-C代码时却很少这么做。这种写法的问题是:对象布局在编译期(compile time)就已经固定了。只要碰到访问firstName变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。这样做目前来看没问题,但是如果又加了一个实例变量,那就麻烦了。比如说,假设在_firstName之前又多了一个实例变量:

@interface EOCPerson : NSObject {

  @public
  NSDate *_dataOfBirth;                            .
  NSString *_firstName;                            .
  NSString *_lastName;

  @private
  NSString *_someInternalData;
}
@end

原来表示_firstName的偏移量现在却指向dateOfBirth 了。把偏移量硬编码于其中的那些代码都会读取到错误的值。

如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。Objective-C的做法是,把实例变量当做一种存储偏移量所用的“特殊变量” (special variable),交由“类对象”(class object)保管。偏移量会在运行期査找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至可以在运行期向类中新增实例变量,这就是稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。ABI定义了许多内容,其中一项就是生成代码时所应遵循的规范。有了这种“稳固的”(nonfragile)的ABI,我们就可以在 “class-continuation分类”或实现文件中定义实例变量了。所以说,不一定要在接口中把全部实例变量都声明好,可以将某些变量从接口的public区段里移走,以便保护与类实现有关的内部信息。

这个问题还有一种解决办法,就是尽量不要直接访问实例变量,而应该通过存取方法来做。虽说属性最终还是得通过实例变量来实现。在正规的Objective-C编码风格中,存取方法有着严格的命名规范。 所以Objective-C这门语言才能根据名称自动创建出存取方法。这时@property语法就派上用场了,编译器会自动写出一套存取方法, 用以访问给定类型中具有给定名称的变量。

要访问属性,可以使用“点语法”,编译器会把“点语法”转换为对存取方法的调用,使用“点 语法”的效果与直接调用存取方法相同。

然而属性还有更多优势。如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法"(synthesized method)的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为_ firstName与_lastName。也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字:

@implementation EOCPerson 
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName; 
@end

前述语法会将生成的实例变量命名为_myFirstName与_myLastName,而不再使用默认的名字。一般情况下无须修改默认的实例变置名。

若不想令编译器自动合成存取方法,可以自己实现。也可以使用@dynamic关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。

@dynamic data;

属性特质

使用属性时还有一个问题要注意,就是其各种特质(attribute)设定也会影响编译器所生成的存取方法。比如下面这个属性就指定了三项特质:

@property (nonatomic,readwrite,copy) NSString *firstName;

属性可以拥有的特质分为四类:

原子性

在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity:在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的"(atomic),或者说,该操作具备“原子性”) 。如果属性具备nonatomic特质,则不使用同步锁。请注意若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。

读/写权限

  1. 具备readwrite(读写)特质的属性拥有“获取方法”(getter)与“设置方法"(setter) 。若该属性由@synthesize实现,则编译器会自动生成这两个方法。
  2. 具备readonly (只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时,编译器才会为其合成获取方法。你可以用此特质把某个属性对外公开为只读属性,然后在“dass-cominuaticm分类”中将其重新定义为读写属性。

内存管理语义

属性用于封装数据,而数据则要有“具体的所有权语义”(concrete ownership semantic)。 下面这一组特质仅会影响“设置方法”。例如,用“设置方法”设定一个新值时,它是应该 “保留"(retain)此值呢,还是只将其赋给底层实例变量就好?编译器在合成存取方法时,要根据此特质来决定所生成的代码。如果自己编写存取方法,那么就必须同有关属性所具备的特质相符。

  1. assign:“设置方法”只会执行针对“纯量类型”(scalar type,例如CGFloat或 NSInteger等)的简单赋值操作。
  2. strong:此特质表明该属性定义了一种“拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  3. weak:此特质表明该属性定义了一种“非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
  4. __unsafe_unretained:此特质的语义和assign相同,但是它适用于“对象类型”(object type),该特质表达一种“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak有区别。
  5. copy:此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝”(copy)。当属性类型为NSString *时,经常用此特质来保护其封装性, 因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类,表示一种可以修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变”(immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的”(mutable),就应该在设置新属性值时拷贝一份。

方法名

可通过如下特质来指定存取方法的方法名:

  1. getter=<name>指定“获取方法”的方法名。如果某属性是Boolean型,而你想为其获取方法加上“is”前缀,那么就可以用这个办法来指定。
@property (nonatomic, getter=isOn) BOOL on;
  1. setter=<name>指定“设置方法”的方法名。这种用法不太常见。

通过上述特质,可以微调由编译器所合成的存取方法。不过需要注意:若是自己来实现这些存取方法,那么应该保证其具备相关属性所声明的特质。比方说,如果将某个属性声明 为copy,那么就应该在“设置方法”中拷贝相关对象,否则会误导该属性的使用者。

如果想在其他方法里设置属性值,那么同样要遵守属性定义中所宣称的语义。

@interface EOCPerson : NSManagedObject
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
- (id)initWithFirstName: (NSString *)firstName lastName:(NSString *)lastName;
@end

在实现这个自定义的初始化方法时,一定要遵循属性定义中宣称的“copy”语义,因为 “属性定义”就相当于“类”和“待设置的属性值”之间所达成的契约。

- (id)initWithFirstName: (NSString*) firstName lastName:(NSString*)lastName {
  if ((self = [super init])) {
    _firstName = [firstName copy];
    _lastName = [lastName copy];
  }
  return self;
)

为何不调用属性所对应的“设置方法”呢?如果用了“设置方法”的话,不是总能保证准确的语义吗?笔者在第7条中将会详细解释为什么决不应该在init (或dealloc)方法中调用存取方法。

atomic与nonatomic的区别是什么呢?前面说过,具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。这也就是说,如果两个线程读写同一属性,那么不论何时, 总能看到有效的属性值。若是不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。 发生这种情况时,线程读到的属性值可能不对。

一般来说所有属性都声明为nonatomic。这样做的历史原因是:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全"(thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值。 因此,开发iOS程序时一般都会使用nonatomic属性。但是在开发Mac OS X程序时,使用atomic属性通常都不会有性能瓶颈。

要点:

  1. 可以语法来定义对象中所封装的数据。
  2. 通过“特质”来指定存储数据所需的正确语义。
  3. 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
  4. 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

第7条:在对象内部尽量直接访问实例变量

在对象之外访问实例变量时,总是应该通过属性来做。

使用“点语法”通过存取方法来访问相关实例变量和不经由存取方法,而是直接访问实例变量,这两种写法有几个区別:

  1. 由于不经过Objective-C的“方法派发” (method dispatch,参见第11条)步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  2. 直接访问实例变童时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
  3. 如果直接访问实例变量,那么不会触发‘键值观测’(Key-Value Observing,KVO:一种通知机制,当某对象属性改变时,可通知其他对象)通知。这样做是否会产生问题,还取决于具体的对象行为。
  4. 通过属性来访问有助于排査与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点"(breakpoint),监控该属性的调用者及其访问时机。

有一种合理的折中方案,那就是:在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问之。此办法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关属性的“内存管理语义”得以贯彻。但是,选用这种做法时,需注意几个问题。

第一个要注意的地方就是,在初始化方法中应该如何设置属性值。这种情况下总是应该直接访问实例变量,因为子类可能会“覆写”(override)设置方法。假设EOCPerson有一 个子类叫做EOCSmithPerscm,这个子类专门表示那些姓“Smith”的人。该子类可能会覆写lastName属性所对应的设置方法:

- (void)setLastName:(NSString^)lastName {
  if (![lastName isEqualToString:@"Smith"]) {
    [NSException raise:NSInvalidArgumentExceptionformat:@"Last name must be Smith"];
  }
  self.lastName = lastname;
}

但是,某些情况下却又必须在初始化方法中调用设置方法:如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用“设置方法”了。

另外一个要注意的问题是‘惰性初始化’(lazy initialization:由于此属性不常用, 而且该属性所指代的对象相当复杂,创建成本较高)。在这种情况下,必须通过 “获取方法”来访问属性,否则,实例变量就永远不会初始化。

要点:

  1. 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
  2. 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
  3. 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

第8条:理解“对象等同性”这一概念

根据“等同性”(equality)来比较对象是一个非常有用的功能。不过,按照==操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的isEqual:方法来判断两个对象的等同性。一般来说, 两个类型不同的对象总是不相等的(unequal)。某些对象提供了特殊的“等同性判定方法” (equality-checking method),如果已经知道两个受测对象都属于同一个类,那么就可以使用这种方法。以下述代码为例:

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat: @"Badger 123"];
BOOL equalA = (foo == bar); //NO
BOOL equalB = [foo isEqual: bar] ; // YES
BOOL equalC = [foo isEqualToString:bar]; // YES

大家可以看到==与等同性判断方法之间的差别。NSString类实现了一个自己独有的等同性判断方法,名叫isEqualToString:。传递给该方法的对象必须是NSString,否则结果未定义(undefined)。调用该方法比调用isEquah方法快,后者还要执行额外的步骤,因为它不知道受测对象的类型。

NSObject协议中有两个用于判断等同性的关键方法:

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当其“指针值"(pointer value:理解为内存地址)完全相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其约定(contract)。如果isEqual:方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么isEqual:方法未必会认为两者相等。

比如有下面这个类:

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end

我们认为,如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。于是isEqual:方法可以写成:

- (BOOL)isEqual:(id)object {
  if (self == object) return YES;
  if ([self class] != [object class]) return NO;
  EOCPerson *otherPerson = (EOCPerson *)object;
  if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
  if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
  if (_age != otherPerson.age) return NO; 
  return YES;
}

首先,直接判断两个指针是否相等。若相等,则其均指向同一对象,所以受测的对象也必定相等。接下来,比较两对象所属的类。若不属于同一个类,则两对象不相等。EOCPerson对象当然不可能与EOCDog对象相等。不过,有时我们可能认为:一个EOCPerson实例可以与其子类(比如EOCSmithPerson)实例相等。在继承体系(inheritance hierarchy)isEqual:情况。最后,检测每个属性是否相等。只要其中有不相等的属性,就判定两对象不等,否则两对象相等。

接下来该实现hash方法了。回想一下,根据等同性约定:若两对象相等,则其哈希码(hash:也叫做“散列”)也相等,但是两个哈希码相同的对象却未必相等。这是能否正确覆写isEqual:方法的关键所在。下面这种写法完全可行:

- (NSUInteger)hash {
  return 1337;
}

不过若是这么写的话,在collection中使用这种对象将产生性能问题,因为collection在检索哈希表(hash table)时,会用对象的哈希码做索引。假如某个collection是用set(collectionset在中文里都叫做“集合”,前者是Array、Dictionary、Set等数据结构的总称。为避免混淆,保留这两个词的英文写法。) 实现的, 那么set可能会根据哈希码把对象分装到不同的数组(这种数组在后文中也称为“箱子"(bin))中。在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检査其中各个元素,看数组中已有的对象是否和将要添加的新对象相等。如果相等,那就说明要添加的对象已经在set里面了。
再来看最后一种计算哈希码的办法:

- (NSUInteger)hash {
  NSUInteger firstNameHash =[_firstName hash】;
  NSUInteger lastNameHash = [_lastName hash];
  NSUInteger ageHash = _age;
  return firstNameHash ^ lastNameHash ^ ageHash;
}

这种做法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁地重复。当然,此算法生成的哈希码还是会碰撞(collision),不过至少可以保证哈希码有多种可能的取值。编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。

特定类所具有的等同性判定方法

NSArray与NSDictionary类也具有特殊的等同性判定方法,前者名为“isEqualToArray:”,后者名为“isEqualToDictionary:”。如果和其相比较的对象不是数组或字典,那么这两个方法会各自抛出异常。由于Objective-C在编译期不做强类型检査(strong type checking),这样容易不小心传入类型错误的对象,因此开发者应该保证所传对象的类型是正确的。

如果经常需要判断等同性,那么可能会自己来创建等同性判定方法,因为无须检测参数类型,所以能大大提升检测速度。自己来编写判定方法的另一个原因是,我们想令代码看上去更美观、更易读,此动机与NSString类“isEqualToString:”方法的创建缘由相似,纯粹为了装点门面。使用此种判定方法编出来的代码更容易读懂,而且不用再检査两个受测对象的类型了。

在编写判定方法时,也应一并覆写“isEqual:”方法。后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自已编写的判定方法,否则就交由超类来判断。例如,在EOCPerson类中可以实现如下两个方法:

- (BOOL)isEqualToPerson:(EOCPerson^)otherPerson { 
  if (self == object) return YES;
  if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
  if (![_lastName isEqualToString:otherPerson.lastName]) return NO; 
  if (_age != otherPerson.age) return NO; 
  return YES;
}

- (BOOL)isEqual:(id)object {
  if ([self class] == [object class]) {
    return [self isEqualToPerson:(EOCPerson*)object];
  } else {
    return [super isEqual:object];
}

等同性判定的执行深度

创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。NSArray的检测方式为先看两个数组所含对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEquth”方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”(deep equality)。不过有时候无须将所有数据逐个比较,只根据其中部分数据即可判明二者是否等同(比方说唯一标识符)。
是否需要在等同性判定方法中检测全部字段取决于受测对象。只有类的编写者才可以确 定两个对象实例在何种情况下应判定为相等。

容器中可变类的等同性

还有一种情况一定要注意,就是在容器中放入可变类对象的时候。把某个对象放入collection之后,就不应再改变其哈希码了。前面解释过,collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后哈希码又变了,那么其现在所处的这个箱子对它来说就是“错误”的。要想解决这个问题,需要确保哈希码不是根据对象的“可变部分”(mutable portion)计算出来的,或是保证放入collection之后就不再改变对象内容了。笔者将在第18条中解释为何要将对象做成“不可变的"(immutable)。这里先举个例子,此例能很好地说明其中缘由。

用一个NSMutableSet与几个NSMutableArray对象测试一下,就能发现这个问题了。
set中居然可以包含两个彼此相等的数组!根据set的语义是不允许出现这种情况的,然而现在却无法保证这一点了,因为我们修改了set中已有的对象。若是拷贝此set,那就更糟糕了,复制过的set中又只剩一个对象了,此set看上去好像是由一个空set开始、通过逐个向其中添加新对象而创建出来的。这可能符合你的需求,也可能不符合。有的开发者也许想要忽略set中的错误,“照原样”(verbatim)复制一个新的出来,还有的开发者则会认为这样做挺合适的。这两种拷贝算法都说得通,于是就进一步印证了刚才提到的那个问题:如果把某对象放入set之后又修改其内容,那么后面的行为将很难预料。

要点:

  1. 若想检测对象的等同性,请提供“isEqual:”与hash方法。
  2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
  3. 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
  4. 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的箅法。

第9条 以“类族模式”隐藏实现细节

“类族”(class cluster:也叫做类簇)是一种很有用的模式(pattern),可以隐藏“抽象基类”(abstract base class)背后的实现细节。Objective-C的系统框架中普遍使用此模式。比如UIButton:

+ (UIButton*)buttonWithType:(UIButtonType)type;

该方法所返回的对象,其类型取决于传入的按钮类型(button type)。然而,不管返回什么类型的对象,它们都继承自同一个基类:UIButton。这么做的意义在于:UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。

类族模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须自己创建子类实例,只需调用基类方法来创建即可。

创建类族

现在举例来演示如何创建类族。假设有一个处理雇员的类,每个雇员都有“名字”和 “薪水”这两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容却不同。 经理在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。

首先要定义抽象基类:

typedef NS_ENUM(NSUInteger EOCEmployeeType) { 
  EOCEmployeeTypeDeveloper, 
  EOCEmployeeTypeDesigner, 
  EOCEmployeeTypeFinance
};

@interface EOCEmployee : NSObject
@property (copy) NSString *name;
@property NSUInteger salary;
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
- (void)doADaysWork;
@end

@implementation EOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type { 
  switch (type) {
  case EOCEmployeeTypeDeveloper:
    return [EOCEmployeeDeveloper new];
    break;

  case EOCEmployeeTypeDesigner:
    return (EOCEmployeeDesigner new]; 
    break;

  case EOCEmployeeTypeFinance:
    return (EOCEmployeeFinance new]; 
    break;
  }
}

- (void)doADaysWork {
  // Subclasses implement this.
}
@end

每个“实体子类"(concrete subclass) 都从基类继承而来。例如:

@interface EOCEmployeeDeveloper : EOCEmployee 
@end

@implementation EOCEmployeeDeveloper
@end  

在本例中,基类实现了一个“类方法”,该方法根据待创建的雇员类别分配好对应的雇员类实例。这种“工厂模式”(Factory pattern)是创建类族的办法之一。

如果对象所属的类位于某个类族中,那么在査询其类型信息(introspection:是某些面向对象语言可以在运行期检视对象类型与属性的一种功能。中文译作 “内省”或“类型内省”)时就要当心了。你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。在Employee这个例子中,[employee isMemberOfClass:[EOCEmployee class]]返回的却是NO,因为employee并非Employee类的实例,而是其某个子类的实例。

Cocoa里的类族

系统框架中有许多类族。大部分collection类都是类族(作者有时把“类族中的抽象基类”(the abstract base class of a class cluster)直接称为“类族”。这句话实际上是说,大部分collection类都是某个类族中的抽象基类),例如NSArray与其可变版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以合起来算作一个类族(在传统的类族模式中,通常只有一个类具备“公共接口”(public imerface),这个类就是类族中的抽象基类)。不可变的类定义了对所有数组都通用的方法,而可变的类则定义了那些只适用于可变数组的方法。两个类共属同一类族,这意味着二者在实现各自类型的数组时可以共用实现代码,此外,还能够把可变数组复制为不可变数组,反之亦然。

在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某类的实例, 此实例充当“占位数组”(placeholder array)。该数组稍后会转为另一个类的实例,而那个类则是NSArray的实体子类。这个过程稍显复杂,其完整的解释已经超出本书范围。

像NSArray这样的类的背后其实是个类族(对于大部分collection类而言都是这样),明白这一点很重要,否则就可能会写出下面这种代码:

id maybeAnArray = /* ••• */;
if ([maybeAnArray class) == [NSArray class]) {
  //Will never be hit
}

你要是知道NSArray是个类族,那就会明白上述代码错在哪里:其中的if语句永远不可能为真。[maybeAnArray class]所返回的类绝不可能是NSArray类本身,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口(public facade)后面的某个内部类型(internal type)。

若想判断出某个实例所属的类是否位于类族之中或者判断某对象是否位于类族中,不要直接检测两个“类对象”是否等同,而应该改用类型信息查询方法(introspectionmethod):

id maybeAnArray = /*••• */;
if([maybeAnArray isKindOfClass:[NSArray class])) {
  //Will be hit
}

我们经常需要向类族中新增实体子类,不过这么做的时候得留心。在Employee这个例子中,若是没有“工厂方法”(factory method)的源代码,那就无法向其中新增雇员类别了。 然而对于Cocoa中NSArray这样的类族来说,还是有办法新增子类的,但是需要遵守几条规则。这几条规则如下。

  1. 子类应该继承自类族中的抽象基类;
  2. 子类应该定义自己的数据存储方式:因为NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说, 可以用NSArray来保存其实例。
  3. 子类应当覆写超类文档中指明需要覆写的方法:在每个抽象基类中,都有一些子类必须覆写的方法。比如说,想要编写NSArray的子 类,就需要实现countobjectAtlndex:方法。像lastObject这种方法则无须实现,因为基类可以根据前两个方法实现出这个方法。

在类族中实现子类时所需遵循的规范一般都会定义于基类的文档之中,编码前应该先看看。

要点:

  1. 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  2. 系统框架中经常使用类族。
  3. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

第10条:在既有类中使用关联对象存放自定义数据

有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。还有一种方法,这就是“关联对象"(Associated Object)。

可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可 以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义,表2-1列出了该枚举的取值,同时还列出了与之等效的属性:假如关联对象成为了属性,那么它就会具备对应的语义。


对象关联类型.png

下列方法可以管理关联对象:

  1. objc_setAssociatedObject(id object, void *key, id value, objc AssociationPolicy policy):此方法以给定的键和策略为某对象设置关联对象值。
  2. objc_getAssociatedObject(id object, void *key):此方法根据给定的键从某对象中获取相应的关联对象值。
  3. objc_removeAssociatedObjects(id object):此方法移除指定对象的全部关联对象。

我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKey:key][object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时用的键(key)是个‘不透明的指针’(opaque pointer) ”。如果在两个键上调用isEqual:方法的返回值是YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时, 通常使用静态全局变量做键。

关联对象用法举例

开发iOS时经常用到UIAlertView类,如果想在同一个类里处理多个警告信息视图,那么代码就会变得更为复杂,我们必须在delegate方法中检査传入的alertView参数,并据此选用相应的逻辑。要是能在创建警告视图的时候直接把处理每个按钮的逻辑都写好,那就简单多了。这可以通过关联对象来做。创建完警告视图之后,设定一个与之关联的‘块"(block)。以这种方式改写之后,创建警告视图与处理操作结果的代码都放在一起了,这样比原来更易读。但是, 采用该方案时需注意:块可能要捕获(capture)某些变量,这也许会造成“保留环”(retain cycle)。

正如大家所见,这种做法很有用,但是只应该在其他办法行不通时才去考虑用它。若是滥用,则很快就会令代码失控,使其难于调试。“保留环”产生的原因很难査明,因为关联对象之间的关系并没有正式的定义(formal definition),其内存管理语义是在关联的时候才定义的,而不是在接口中预先定好的。使用这种写法时要小心,不能仅仅因为某处可以用该写 就一定要用它。想创建这种UIAlertView还有个办法,那就是从中继承子类,把块保存为子类中的属性。笔者认为:若是需要多次用到alert视图,那么这种做法比使用关联对象要好。

要点:

  1. 可以通过“关联对象”机制来把两个对象连起来。
  2. 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
  3. 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于査找的bug。