iOS开发读书笔记:Effective Objective-C

2018-11-26  本文已影响23人  Ryan___

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

第11条:理解objc_msgSend的作用

在对象上调用方法是Objective-C中经常使用的功能。用Objective-C的术语来说,这叫做“传递消息”(pass a message)。
由于Objective-C是C的超集,所以最好先理解C语言的函数调用方式。C语言使用“静态绑定”(static binding),也就是说,在编译期就能决定运行时所应调用的函数。

import <stdio.h>

void printHello {
  printf ("Hello, world! \n");
}

void printGoodbye() {
  printf ("Goodbye, world! \n");
}

void doTheThing(int type) { 
  if (type == 0) {
  printHello();
} else  {
  printGoodbye();
}
  return 0;
}

如果不考虑“内联”(inline),那么编译器在编译代码的时候就已经知道程序中有该函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。若是将刚才那段代码写成下面这样,会如何?

void doTheThing(int type) { 
  void (*fnc)(); 
  if (type == 0) {
    fnc = printHello;
  } else {
    fnc = printGoodbye;
  }
  fnc (); 
  return 0;
}

这时就得使用“动态绑定”(dynamic binding) 了,因为所要调用的函数直到运行期才能确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,if与else语句里都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。

在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
给对象发送消息可以这样来写:

id returnValue = [someObject messageName:parameter];

在本例中,someObject 叫做“接收者”(receiver), messageName 叫做“选择子”(selector),选择子指的就是方法的名字, “选择子”与“方法”这两个词经常交替使用。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其 “原型"(prototype)如下:

void objc_msgSend(id self, SEL cmd,...)

编译器会把刚才那个例子中的消息转换为如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:), parameter);

每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是査表时所用的“键”。objC_mSgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作, 该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上査找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发” (message forwarding)操作。

这么说来,想调用一个方法似乎需要很多步骤。所幸objc_mSgSend会将匹配结果缓存在“快速映射表"(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然这种“快速执行路径”(fastpath)还是不如 “静态绑定的函数调用操作’(statically bound function call)那样迅速,不过只要把选择子缓存起来,也就不会慢很多,实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。假如真是个瓶颈的话,那你可以只编写纯C函数,在调用时根据需要,把ObjectWe-C对象的状态传进去。

前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况"(edgecase)。则需要交由Objective-C运行环境中的另一些函数来处理:

  1. objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  2. objc_mSgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器’(floating-point register)做特殊处理, 也就是说,通常所用的objC_msgSend在这种情况下并不合适。这个函数是为了处理 x86等架构CPU中某些令人稍觉惊讶的奇怪状况。
  3. objc_msgSendSuper :如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息。

利用“尾调用优化”技术,令“跳至方法实现”这一操作变得更简单些。

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。 编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧"(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时, 才能执行“尾调用优化”。这项优化对objc_mSgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objC_mSgSend函数准备“栈帧”,大家在“栈踪迹”(stack trace)中可以看到这种“栈帧”。此外,若是不优化,还会过早地发生“栈溢出” (stack overflow)现象。

这样也就理解,为何在调试的时候,栈“回溯”(backtrace)信息中总是出现objC_mSgSend。

要点:

  1. 消息由接收者、选择子及参数构成。给某对象泼送消息"(invoke a message:也是“调用”的意思,此处为了与“call”相区隔,将其临时译为“发送”,也可理解为“激发”、 “触发)”也就相当于在该对象上“调用方法”(call a method)。
  2. 发给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system) 来处理,该系统会査出对应的方法,并执行其代码。

第12条:理解消息转发机制

上面讲解了对象的消息传递机制,本节讲解对象在收到无法解读的消息之后会发生什么情况。

若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发"(message forwarding)机制,程序员可经由此过程告沂对象应该如何处理未知消息。

你可能早就遇到过经由消息转发流程所处理的消息了,只是未加留意。如果在控制台中看到下面这种提示信息,那就说明你曾向某个对象发送过一条其无法解读的消息,从而启动了消息转发机制,并将此消息转发给了NSObject的默认实现。

- [_NSCFNumber lowercasestring]: unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception
'NSInvalidArgumentException',reason: '-[_ NSCFNumber lowercasestring]: unrecognized selector sent to instance 0x87•

上面这段异常信息是由NSObject的doesNotRecognizeSelector:方法所抛出的,此异常表明:消息接收者的类型是__NSCFNumber,而该接收者无法理解名为lowercaseString的选择子。本例所列举的这种情况并不奇怪,因为NSNumber类里本来就没有名为lowercaseString的方法。控制台中看到的那__NSFCNumber是为了实现“无缝桥接"(toll-free bridging,后续将会详解此技术)而使用的内部类(internal class),配置NSNumber对象时也会一并创建此对象。在本例中,消息转发过程以应用程序崩溃而告终,不过,开发者在编写自己的类时,可于转发过程中设置挂钩,用以执行预定的逻辑,而不使应用程序崩溃。

消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子"(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。第二阶段涉及“完整的消息转发机制”(ftill forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有“备援的接收者”(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

+ (BOOL)resolvelnstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与resolvelnstanceMethod: 类似,叫做resolveClassMethod:

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性,因为实现这些属性所需的存取方法在编译期就能确定。

备援接收者

当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到, 就返回nil。通过此方案,我们可以用“组合”(composition)来模拟出“多重继承”(multiple inheritance)的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。

请注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。

完整的消息转发

如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制 了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。 此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统” (message-dispatch system)将亲自出马,把消息指派给目标对象。

此步骤会调用下列方法来转发消息:

- (void)forwardlnvocation:(NSInvocation *)invocation

这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了 NSObject类的方法,那么该方法还会继而调用doesNotRecognizeSelector:以抛出异常, 此异常表明选择子最终未能得到处理。

消息转发全流程

消息转发机制处理消息的各个步骤.png

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。

以完整的例子演示动态方法解析

为了说明消息转发机制的意义,下面示范如何以动态方法解析来实现@dynamic属性。将属性声明为@dynamic,这样的话,编译器就不会为其自动生成实例变量及存取方法了

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject 
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingstore;
@end

@implementation EOCAutoDictionary
@dynamic date,opaqueObject;

- (id)init {
  if ([self == [super init]) {
    _backingStore = [NSMutableDictionary new];
  }
  return self;
}

+ (BOOL)resolvelnstanceMethod:(SEL)selector {
  NSString *selectorstring = NSStringFromSelector(selector); 
  if ([selectorstring hasPrefix: @"set"]) { 
    class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@");
  } else {
    class_addMethod(self,selector,(IMP)autoDictionarySetter,"@@:");
  }
  return YES;
}

//getter函数可以用下列代码实现:
id autoDictionaryGetter(id self, SEL _cmd) {
  //Get the backing store from the object
  EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
  NSMutableDictionary *backingStore = typedSelf.backingStore;
  //Thekey is simply the selector name 
  NSString *key = NSStringFromSelector(_cmd);
  // Return the value
  return [backingStore objectForKey:key];
}

//setter函数则可以这么写:
void autoDictionarySetter(id self, SEL _cmd, id value) {
  //Getthe backing store from the object
  EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
  NSMutableDictionary *backingStore = typedSelf.backingStore;
  //The selector will be for example, "setOpaqueObject: ". We need to remove the "set",and lowercase the first letter of the remainder.
  NSString *selectorstring = NSStringFromSelector(_cmd);
  NSMutablestring *key = [selectorstring mutableCopy];
  // Remove the ':' at the end 
  [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
  // Remove the 'set' prefix
  [key deleteCharactersInRange:NSMakeRange(0,3)];
  // Lowercase the first character
  NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercasestring];
  [key replaceCharactersInRange:NSMakeRange(0,1) withString:lowercaseFirstChar];
  if (value) {
    [backingStore setObject:value forKey:key];
  } else {
      [backingStore removeObjectForKey:key];
  }
}
@end

当开发者首次在EOCAutoDictionary实例上访问某个属性时,运行期系统还找不到对应的选择子,因为所需的选择子既没有直接实现,也没有合成出来。现在假设要写入opaqueObject属性,那么系统就会以setOpaqueObject:为选择子来调用上面这个方法。 同理,在读取该属性时,系统也会调用上述方法,只不过传入的选择子是opaqueObject

resolvelnslanceMethod方法会判断选择子的前缀是否为set,以此分辨其是set选择子还是get选择子。在这两种情况下,都要向类中新增一个处理该选择子所用的方法,这两个方法分别以autoDictionarySetterautoDictionaryGetter函数指针的形式出现。此时就用到class_addMethod方法,它可以向类中动态地添加方法,用以处理给定的选择子。第三个参数为函数指针,指向待添加的方法。而最后一个参数则表示待添加方法的“类型编码”(type encoding)。在本例中,编码开头的字符表示方法的返回值类型,后续字符则表示其所接受的各个参数。

EOCAutoDictionary的用法很简单:

EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSincel970:475372800];
NSLog(@"dict.date = %@",dict.date);
// Output: diet.date = 1985-01-24 00:00:00 +0000

其他属性的访问方式与date类似,要想添加新属性,只需来定义,并将其声明为@dynamic即可。在iOS的CoreAnimation框架中,CALayer类就用了与本例相似的实现方式,这使得CALayer成为兼容于“键值编码的”(key-value-coding-compliant:除了使用存取方法和“点语法”之外,还可以用字符串做键,通过valueForKey:setValue:forKey:这种形式来访问属性) 容器类, 也就等于说,能够向里面随意添加属性,然后以键值对的形式来访问。于是,开发者就可以向其中新增自定义的属性了,这些属性值的存储工作由基类直接负责,我们只需在CALayer 的子类中定义新属性即可。

要点:

  1. 若对象无法响应某个选择子,则进入消息转发流程。
  2. 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  3. 对象可以把其无法解读的某些选择子转交给其他对象来处理。
  4. 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。

第13条:用“方法调配技术”调试“黑盒方法”

上面讲了:Objective-C对象收到消息之后,方法需要在运行期才能解析出来。那么与给定的选择子名称相对应的方法也可以在运行期改变。若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为 “方法调配”(method swizzling) 。

类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统” 能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP, 其原型如下:

id (*IMP) (id, SEL,…)

NSString 类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上。


NSString类的选择子映射表.png

Objective-C运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。 经过几次操作之后,类的方法表就会变成下图这个样子。


经过数字操作之后的NSString选择子映射表.png

在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了, 而lowercaseString与uppercaseString的实现则互换了。上述修改均无须编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。

在实际应用中,直接交换两个方法实现的意义并不大。我们一般使用该手段来为既有的方法实现增添新功能。 比方说,想要在调用lowercaseString时记录某些信息,这时就可以通过交换方法实现来达成此目标。我们新编写一个方法,在此方法中实现所需的附加功能,并调用原有实现。

// .h 新方法可以添加至NSString的一个“分类”(category)中:
@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end

//.m 新方法的实现代码可以这样写:
@impleinentation NSString (EOCMyAdditions)
+ (void)load {
  //方法实现则可通过下列函数获得:
  Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString)〉;
  Method swappedMethod = class_getInstanceMethod([NSString class],@selector(eoc_myLowercaseString)); 
  //交换方法实现
  method_exchangeImplementations(originalMethod,swappedMethod);
}

- (NSString*)eoc_myLowercaseString {
  NSString *lowercase = [self eoc_myLowercaseString];
  NSLog (@"%@ => %@", self, lowercase);
  return lowercase;
)
@end

从现在开始,如果在NSString实例上调用lowercaseString,那么执行的将是uppercaseString的原有实现,反之亦然。
上述新方法将与原有的lowercaseString方法互换,交换之后的方法表如图:


交互lowercaseString与eoc_myLowercaseString的方法实现.png

这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。

通过此方案,开发者可以为那些“完全不知道其具体实现的"(completely opaque, “完全不透明的”)黑盒方法增加日志记录功能,这非常有助于程序调试。然而,此做法只在调试程序时有用。很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。不能仅仅因为Objective-C语言里有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护。

要点:

  1. 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  2. 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此 技术向原有实现中添加新功能。
  3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

第14条:理解“类对象”的用意

Objective-C实际上是一门极其动态的语言。第11条讲解了运行期系统如何査找并调用某方法的实现代码,第12条则讲述了消息转发的原理:如果类无法立即响应某个选择子, 那么就会启动消息转发流程。然而,消息的接收者究竟是何物?是对象本身吗?运行期系统如何知道某个对象的类型呢?对象类型并非在编译期就绑定好了,而是要在运行期査找。而且,还有个特殊的类型叫做id,它能指代任意的Objective-C对象类型。一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。

如果看过第12条,你就会明白,编译器无确定某类型对象到底能解读多少种选择子, 因为运行期还可向其中动态新增。然而,即便使用了动态新增技术,编译器也觉得应该能在某个头文件中找到方法原型的定义,据此可了解完整的“方法签名"(method signature),并生成派发消息所需的正确代码。

“在运行期检视对象类型”这一操作也叫做“类型信息査询”(introspection, “内省”),这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息査询方法”,其原因笔者稍后解释。不过在介绍类型信息査询技术之前,我们先讲一些基础知识,看看Objective-C对象的本质是什么。

每个Objective-C对象实例都是指向某块内存数据的指针(???)。所以在声明变量时,类型后面要跟一个字符:

NSString *pointerVariable = @"Some string";

编过C语言程序的人都知道这是什么意思。对于没写过C语言的程序员来说, pointerVariable可以理解成存放内存地址的变量,而NSString自身的数据就存于那个地址中。 因此可以说,该变量“指向”(point to) NSString实例。所有Objective-C对象都是如此,若是想把对象所需的内存分配在栈上,编译器则会报错:

String stackVariable = @"Some string";
"error: interface type cannot be statically allocated

对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:

id genericTypedString = @"Some string";

上面这种定义方式与用NSString *来定义相比,其语法意义相同。唯一区别在于,如果声明时指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。

描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:

typedef struct objc_object {
  Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类, 通常称为isa指针。例如,刚才的例子中所用的对象“是一个”(isa) NSString,所以其“isa”指针就指向NSString。
Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class {
  Class isa;
  Class super_class;
  const char *name;
  long version;
  long info;
  long instance_size;
  struct objc_ivar_list *ivars;
  struct objc_method_list *methodLists;
  struct objc_cache *cache;
  struct objc_protocol list *protocols;
);

此结构体存放类的“元数据"(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。 结构体里还有个变量叫做superclass,它定义了本类的超类。类对象所属的类型(也就是isa 指针所指向的类型)是另外一个类,叫做“元类"(metadass),用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如图


SomeClass实例所属的“类继承体系”,此类继承自NSObject,图中也画出了两个对应“元类”之间的继承关系.png

superclass指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关图即可执行“类型信息査询”。我们可以査出对象是否能响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”(class hierarchy)的哪一部分。

在类继承体系中查询类型信息

可以用类型信息査询方法来检视类继承体系。isMemberOfClass:能够判断出对象是否为某个特定类的实例,而isKindOfClass:则能够判断出对象是否为某类或其派生类的实例。例如:

NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass: [NSDictionary class] ] ; //<NO 
[dict isMemberOfClass:[NSMutableDictionary class]];//< YES 
[dict isKindOfClass: [NSDictionary class]];//< YES 
[dict isKindOfClass:[NSArray class]]; //< NO

像这样的类型信息査询方法使用isa指针获取对象所属的类,然后通过superclass指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。Objective-C与你可能熟悉的其他语言不同,在此语言中,必须査询类型信息,方能完全了解对象的真实类型。

由于Objective-C使用“动态类型系统"(dynamic typing),所以用于査询对象所属类的类型信息査询功能非常有用。从collection中获取对象时,通常会査询类型信息,这些对象不是“强类型的”(strongly typed),把它们从collection中取出来时,其类型通常是id。如果想知道具体类型,那就可以使用类型信息査询方法。

也可以用比较类对象是否等同的办法来做。若是如此,那就要使用==操作符,而不要使用比较Objective-C对象时常用的“isEqual:”方法(参见第8条)。原因在于,类对象是 “单例”(singleton),在应用程序范围内,每个类的Class仅有一个实例。也就是说,另外一种可以精确判断出对象是否为某类实例的办法是:

id object = /* ••• */;
if ([object class] == [EOCSomeClass class]) {
  //'object' is an instance of EOCSomeClass
)

即便能这样做,我们也应该尽景使用类型信息査询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制(参见第12条)的对象。比方说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理” (proxy),此种对象均以NSProxy为根类。

通常情况下,如果在此种代理对象上调用class方法,那么返回的是代理对象本身(此类是NSProxy的子类),而非接受的代理的对象所属的类。然而,若是改用isKindOfClass: 这样的类型信息査询方法,那么代理对象就会把这条消息转给“接受代理的对象”(proxied object)。也就是说,这条消息的返回值与直接在接受代理的对象上面査询其类型所得的结果相同。因此,这样査出来的类对象与通过class方法所返回的那个类对象不同,class方法所返回的类表示发起代理的对象,而非接受代理的对象。

要点:

  1. 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
  2. 如果对象类型无法在编译期确定,那么就应该使用类型信息査询方法来探知。
  3. 尽量使用类型信息査询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

第三章 接口与API设计

第15条:用前缀避免命名空间冲突

Objective-C没有其他语言那种内置的命名空间(namespace)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。如果发生命名冲突(naming dash), 那么应用程序的链接过程就会出错,因为其中出现了重复符号:

duplicate symbol _OBJC_METACLASS_$_EOCTheClass in: build/something.o build/something_else.o
duplicate symbol _OBJC_CLASS_$—EOCTheClass in: build/something.o build/something_else.o

避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或二者皆有关联之名。Apple宣称其保留使用所有“两字母前缀” (two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。

不仅是类名,还有以下:

  1. 那么一定要给“分类” (category)及“分类”中的方法加上前缀(第25条解释了这么做的原因)。
  2. 类的实现文件中所用的纯C函数及全局变量,这个问题必须要注意。因为在编译好的目标文件中,这些名称是要算作“顶级符号”(top-level symbol)的。

这么做的好处:若此符号出现在栈回溯信息中,则很容易就能判明问题源自哪块代码。

要点:

  1. 选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。
  2. 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。

第16条: 提供“全能初始化方法”

为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer) 。令其他初始化方法都来调用它。只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。

要点:

  1. 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  2. 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  3. 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

第17条:实现description方法

调试程序时,经常需要打印并查看对象信息。构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”(format string:在这里指%@)。

当自定义对象,并想输出更为有用的信息也很简单,只需覆写description方法并将描述此对象的字符串返回即可。

NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与description非常相似。二者区别在于,debugDescription方法是开发者在调试器(debugger) 中以控制台命令(比如LLDB的po命令)打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description 。

要点:

  1. 实现description方法返回一个有意义的字符串,用以描述该实例。
  2. 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。

第18条:尽量使用不可变对象

设计类的时候,应充分运用属性来封装数据(参见第6条)。默认情况下,属性是“既可读又可写的"(read-write),这样设计出来的类都是“可变的”(mutable)。具体到编程实践中,则应该尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。

当然,如果该属性是nonatomic的,那么这样做可能会产生“竞争条件”(racecondition)。在对象内部写入某属性时,对象外的观察者也许正读取该属性。若想避免此问题,我们可以在必要时通过“派发队列"(dispatch queue, 参见第41条)等手段,将(包括对象内部的)所有数据存取操作都设为同步操作。

kvc和类型查询:

  1. 现在,只能于实现代码内部设置这些属性值了。其实更准确地说, 在对象外部,仍然能通过“键值编码”(Key-Value Coding,KVC)技术设置这些属性值,比如说:[pointOfInterest setValue:@"abc forKey:@"identifier"];这样做可以改动identifier属性,因为KVC会在类里査找setIdentifier:方法,并借此修改此属性。不过,这样做等于违规地绕过了本类所提供的API。
  2. 有些“爱用蛮力的”(brutal)程序员甚至不通过“设置方法”,而是直接用类型信息查询功能査出属性所对应的实例变量在内存布局中的偏移量,以此来人为设置这个实例变量的值。这样做比绕过本类的公共API还要不合规范

对于集合:

  1. 如果把可变对象(mutable object)放入collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。因此,笔者建议大家尽量减少对象中的可变内容。
  2. 对象里表示各种collection的那些属性究竞应该设成可变的,还是不可变的。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set, 而此set则是内部那个可变set的一份拷贝。

要点:

  1. 尽量创建不可变的对象。
  2. 若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性,属性的其他特质必须保持不变。
  3. 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

第19条:使用清晰而协调的命名方式

方法与变量名使用了“驼峰式大小写命名法"(camel casing)-以小写字母开头,其后每个单词首字母大写。类名也用驼峰命名法,不过其首字母要大写,而且前面通常还有两三个前缀字母(参见第15条)。在编写Objective-C代码时,大家一般都使用这种命名方式。

给方法命名时的注意事项可总结成下面几条规则:

  1. 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
  2. 应该把表示参数类型的名词放在参数前面。
  3. 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加上一个或多个名词。
  4. 不要使用str这种简称,应该用string这样的全称。
  5. Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
  6. 将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言式数组”(C-style array)里的那种方法就可以使用这个词做前缀。

类与协议的命名

应该为类与协议的名称加上前缀,以避免命名空间冲突(参见第15条),如果要从其他框架中继承子类,那么务必遵循其命名惯例。比方说,要从UlView类中继承自定义的子类, 那么类名末尾的词必须是View。同理,若要创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上Delegate—词。

要点:

  1. 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所 理解。
  2. 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
  3. 方法名里不要使用缩略后的类型名称。
  4. 给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。

第20条:为私有方法名加前缀

具体使用何种前缀可根据个人喜好来定,其中最好包含字母P与下划线。比如

- (void)p_add {
  //...
}

如果写过C++或Java代码,你可能就会问了:为什么要这样做呢?直接把方法声明成私有的不就好了吗? Objective-C语言没办法将方法标为私有。每个对象都可以响应任意消息(参见第12条),而且可在运行期检视某个对象所能直接响应的消息(参见第14条)。根据给定的消息査出其对应的方法,这一工作要在运行期才能完成(参见第11条),所以 Objective-C中没有那种约束方法调用的机制用以限定谁能调用此方法、能在哪个对象上调用此方法以及何时能调用此方法。开发者会在命名惯例中体现出“私有方法”等语义。必须用心领悟Objective-C语言这种强大的动态特性。

要点:

  1. 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
  2. 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的

第21条:理解Objective-C错误模型

当前很多种编程语言都有“异常”(exception)机制,Objective-C也不例外。写过Java 代码的程序员应该很习惯于用异常来处理错误。如果你也是这么使用异常的,那现在就把它忘了吧,我们得从头学起。

首先要注意的是,“自动引用计数”(Automatic ReferenceCounting, ARC,参见第30条)在默认情况下不是“异常安全的"(exception safe)。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码, 可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions

既然异常只用于处理严重错误(fatal error,致命错误),那么对其他错误怎么办呢?在出现“不那么严重的错误"(nonfatalerror,非致命错误)时,Objective-C语言所用的编程范式为: 令方法返回nil/0,或是使用NSError,以表明其中有错误发生。

要点:

  1. 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
  2. 在错误不那么严重的情况下,可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。

第22条:理解NSCopying协议

使用对象时经常需要拷贝它。在Objective-C中,此操作通过copy方法完成。如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone *)zone

为何会出现NSZone呢?因为以前开发程序时,会据此把内存分成不同的“区”(zone), 而对象会创建在某个区里面。现在不用了,每个程序只有一个区:“默认区”(default zone)。 所以说,尽管必须实现这个方法,但是你不必担心其中的zone参数。

copy->_friends = [_friends mutableCopy];

这次所实现的方法比原来多了一些代码,它把本对象的_friends实例变量复制了一份, 令copy对象的_friends实例变量指向这个复制过的set。(注意:这里使用了->语法,因为 friends并非属性,只是个在内部使用的实例变量。其实也可以声明一个属性来表示它,不过由于该变量不会在本类之外使用,所以那么做没必要)

NSMutableCopying的协议。该协议与 NSCopying类似,也只定义了一个方法,然而方法名不同:

- (id)mutableCopyWithZone:(NSZone*)zone

在编写拷贝方法时,还要决定一个问题,就是应该执行“深拷贝”(deep copy)还是“浅拷贝”(shallowcopy)。深拷贝的意思就是:在拷贝对象自身时,将其底层数据也一并复制过去。Foundation框架中的所有collection类在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的主要原因在于,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象。

以NSSet为例,该类提供了下面这个初始化方法,用以执行深拷贝:


浅拷贝与深拷贝对比图。浅拷贝之后的内容与原始内容均指向相同对象。 而深拷贝之后的内容所指的对象是原始内容中相关对象的一份拷贝.png
-(id)initWithSet:(NSArray^)array copyltems:(BOOL)copyltems

若copyltem参数设为YES,则该方法会向数组中的每个元素发送copy消息,用拷贝好的元素创建新的set,并将其返回给调用者。

要点:

  1. 若想令自己所写的对象具有拷贝功能,需实现NSCopying协议。
  2. 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与 NSMutableCopying 协议。
  3. 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。

第四章 协议与分类

Objective-C语言有一项特性叫做“协议”(protocol),它与Java的“接口"(interface)类似,Objective-C不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里面。 协议最为常见的用途是实现委托模式(参见第23条),不过也有其他用法。
“分类”(Category)也是Objective-C的一项重要语言特性。利用分类机制,我们无须继承子类即可直接为当前类添加方法,而在其他编程语言中,则需通过继承子类来实现。由于Objective-C运行期系统是髙度动态的,所以才能支持这一特性,然而,其中也隐藏着一些陷阱。

第23条:通过委托与数据源协议进行对象间通信

对象之间经常需要相互通信,而通信方式有很多种。Objective-C开发者广泛使用一种名叫“委托模式"(Delegatepattem)的编程设计模式来实现对象间的通信,该模式的主旨是: 定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其“委托对象”(delegate)。而这“另一个对象”则可以给其委托对象回传一些信息。

在Objective-C中,一般通过“协议”这项语言特性来实现此模式:

  1. 委托协议名通常是在相关类名后面加上Delegate—词,整个类名采用“驼峰法”来写。
  2. 有了这个协议之后,类就可以用一个属性来存放其委托对象了。
  3. 实现委托对象的办法是声明某个类遵从委托协议(一般都是在“class-continuation分类”里声明的)
  4. 把协议中想实现的那些方法在类里实现出来。

注意点:

  1. 存放委托对象的属性需定义成weak,而非strong,因为两者之间必须为“非拥有关系” (nonowning relationship)。通常情况下,扮演delegate的那个对象也要持有本对象。
  2. 如果要在委托对象上调用可选方法,那么必须提前使用类型信息査询方法respondsToSelector:判断这个委托对象能否响应相关选择子。

大家应该很容易理解此模式为何叫做“委托模式”:因为对象把应对某个行为的责任委托给另外一个类了。也可以用协议定义一套接口,令某类经由该接口获取其所需的数据。委托模式的这一用法旨在向类提供数据,故而又称“数据源模式”(Data Source Pattern)。在此模式中,信息从数据源(Data Source)流向类(Class);而在常规的委托模式中,信息则从类流向受委托者 (Delegate)。
下图演示这两条信息流:在信息源模式中,信息从数据源流向类,而在普通的委托模式中,信息则从类流向受委托者


数据源.png

比方说,用户界面框架中的“列表视图”(list view)对象可能会通过数据源协议来获取要在列表中显示的数据。除了数据源之外,列表视图还有一个受委托者,用于处理用户与列表的交互操作。将数据源协议与委托协议分离,能使接口更加清晰,因为这两部分的逻辑代码也分开了。另外,“数据源”与“受委托者”可以是两个不同的对象。然而一般情况下, 都用同一个对象来扮演这两种角色。

要点:

  1. 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
  2. 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
  3. 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情境下,该模式亦称“数据源协议”(data source protocal)。
  4. 若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。

第24条:将类的实现代码分散到便于管理的数个分类之中

类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件里。 在此情况下,可以通过Objective-C的“分类”机制,把类代码按逻辑划入几个分区中。

现在,类的实现代码按照方法分成了好几个部分。所以说,这项语言特性当然就叫做 “分类”啦。类的基本要素(诸如属性与初始化方法等)都声明在“主实现"(main implementation)里。执行不同类型的操作所用的另外几套方法则归入各个分类中。

使用分类机制之后,依然可以把整个类都定义在一个接口文件中,并将其代码写在一个实现文件里。 此时可以把每个分类提取到各自的文件中去。如果想用分类中的方法,那么要记得在引入主类时一并引入分类的头文件。以下是原因:

  1. 随着分类数量增加,当前这份实现文件很快就膨胀得无法管理了。
  2. 便于调试:对于某个分类中的所有方法来说,分类名称都会出现在其符号中,在调试器的回溯信息中会出现。例如,“addFriend:”方法的“符号名”(symbol name):- [EOCPerson(Friendship) addFriend:1];

要点:

  1. 使用分类机制把类的实现代码划分成易于管理的小块。
  2. 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。

第25条:总是为第三方类的分类名称加前缀

分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很容易忽视其中可能产生的问题。这个问题在于:分类中的方法是直接添加在类里面的,它们就好比这个类中的固有方法。将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了“主实现”中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。(注:不是覆盖,是根据selector找到的第一个方法为准,其后不再遍历查找后序的同名方法)

要解决此问题,一般的做法是:以命名空间来区别各个分类的名称与其中所定义的方法。Obiective-C中实现命名空间功能,只有一个办法,就是给相关名称都加上某个共用的前缀。

要点:

  1. 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
  2. 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。

第26条:勿在分类中声明属性

属性是封装数据的方式(参见第6条)。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了 “class-continuation分类”(参见第27条)之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。

有两种办法可以在分类里声明属性:

  1. 把存取方法声明为@dynamic, 也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制(参见第12条)在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。
  2. 关联对象(参见第10条)能够解决在分类中不能合成实例变量的问题。但是在内存管理问题上容易出错。

正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的“语法糖”,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。

虽说如此,但有时候只读属性还是可以在分类中使用的。由于获取方法并不访问数据,而且属性也不需要由实例变量来实现。即便在这种情况下,也最好不要用属性。属性所要表达的意思是:类中有数据在支持着它。属性是用来封装数据的。某些情况下也可以直接声明一个方法,用以获取数据。

要点:

  1. 把封装数据所用的全部属性都定义在主接口里。
  2. 在“dass-contimiation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。

第27条:使用“class-continuation分类”隐藏实现细节

类中经常会包含一些无须对外公布的方法及实例变量。其实这些内容也可以对外公布,并且写明其为私有,但是这样会泄漏实现细节。Objective-C动态消息系统(参见第11条)的工作方式决定了其不可能实现真正的私有方法或私有实例变量。然而,我们最好还是只把确实需要对外公布的那部分内容公开,隐藏部分细节。那么,这个特殊的“class-continuation分类”就派上用场了。

“class-continuation分类”和普通的分类不同,它必须定义在其所接触的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,“class-continuation分类”没有名字。

为什么需要有这种分类呢?因为其中可以定义方法和实例变量。为什么能在其中定义方法和实例变量呢?只因有“稳固的ABI”这一机制(第6条详解了此机制),使得我们无须知道对象大小即可使用它。由于类的使用者不一定需要知道实例变量的内存布局,所以,它们也就未必非得定义在公共接口中了。基于上述原因,我们可以像在类的实现文件里那样,于 “ class-contimiatiori分类”中给类新增实例变量。

实例变量也可以定义在实现块里,从语法上说,这与直接添加到“class-continuation 分类”等效,只是看个人喜好了。笔者喜欢将其添加在“dass-continuation分类”中。这些实例变量并非真的私有,因为在运行期总可以调用某些方法绕过此限制,不过,从一般意义上来说,它们还是私有的。

最后还要讲一种用法:比方说,EOCPerson遵从了名为EOCSecretDelegate的协议。声明在公共接口里。你可能会说,只需要向前声明EOCSecretDelegate协议就可以不引入它了(或者说,不引入定义该协议的头文件了)。用下面这行向前声明语句来取代#import指令:

@protocol EOCSecretDelegate;

但是这样一来,只要引入EOCPerson头文件的地方,由于编译器看不到协议的定义,所以无法得知其中所含的方法,于是编译器会给出瞥告信息。

要点:

  1. 通过“class-continuation分类”向类中新增实例变量。
  2. 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那 么就在“class-continuation分类”中将其扩展为“可读写”。
  3. 把私有方法的原型声明在“class-continuation分类”里面。
  4. 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。

第28条:通过协议提供匿名对象

协议定义了一系列方法,遵从此协议的对象应该实现它们(如果这些方法不是可选的, 那么就必须实现)。于是,我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法——因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。

此概念经常称为“匿名对象"(anonymous object),这与其他语言中的“匿名对象”不同, 在那些语言中,该词是指以内联形式所创建出来的无名类,而此词在Objective-C中则不是这个意思。第23条解释了委托与数据源对象,其中就曾用到这种匿名对象。例如,在定义 “受委托者”(delegate)这个属性时,可以这样写:

@property {nonatomic, weak) id <EOCDelegate> delegate;

由于该属性的类型是id <EOCDelegate>,所以实际上任何类的对象都能充当这一属性, 即便该类不继承自NSObject也可以,只要遵循EOCDelegate协议就行。对于具备此属性的类来说,delegate就是“匿名的”(ammymous)。如有需要,可在运行期査出此对象所属的类型(参见第14条)。然而这样做不太好,因为指定属性类型时所写的那个EOCDelegate契约已经表明此对象的具体类型无关紧要了。

NSDictionary也能实际说明这一概念。在字典中,键的标准内存管理语义是“设置时拷贝”,而值的语义则是“设置时保留”。因此,在可变版本的字典中,设置键值对所用的方法的签名是:

- (void)setObject:(id)object forKey:(id<NSCopying>)key;

表示键的那个参数其类型为id<NSCopying>,作为参数值的对象,它可以是任何类型, 只要遵从NSCopying协议就好,这样的话,就能向该对象发送拷贝消息了。这个key参数可以视为匿名对象。与delegate属性一样,字典也不关心key对象所属的具 体类,而且它也决不应该依赖于此。字典对象只要能确定它可以给此实例发送拷贝消息就行了。

有时对象类型并不重要,重要的是对象有没有实现某些方法,在此情况下,也可以用这些“匿名类型”(anonymous type)来表达这一概念。即便实现代码总是使用固定的类,你可能还是会把它写成遵从某协议的匿名类型,以表示类型在此处并不重要。

要点:

  1. 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
  2. 使用匿名对象来隐藏类型名称(或类名)。
  3. 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
上一篇下一篇

猜你喜欢

热点阅读