禅与Objective-C编程艺术-2

编程和「禅」有关系吗?

Zen and the Art of the Objective-C Craftsmanship译文兼笔记

本章包含规范:类名

类名

类名前应该加上三个大写字母(两个字母为 Apple 的类保留)作为前缀,这个规范虽然看起来有点奇怪,但是可以减少 Objective-C 没有命名空间带来的不便。

有些开发者在定义 Model 对象时不遵循这个规范(尤其对于 Core Data 对象,更应该遵循这个规范),在定义 Core Data 对象时候我们建议严格遵循这个规范,因为你可以最后将你的 Managed Object Model(托管对象)和其他第三方库的 MOMs 合并。

可能你已经注意到了,本书中类名(不止类名)前缀是ZOC

在你给类命名的时候还有一个命名规范:创建子类时,你应该把说明性部分放在类前缀和父类名之间。例如:

有个类名叫ZOCNetworkClient,子类名是ZOCTwitterNetworkClient(注意 “Twitter” 在”ZOC” 和”NetworkClient”之间);遵循相同规范,一个UIViewController的子类应该是ZOCTimelineViewController

Initializer 与 dealloc

比较推荐的代码组织方式是:dealloc方法放在实现文件的最前面(直接在@synthesize@dynamic语句之后),init应该直接放在dealloc之后。要是有多个初始化方法,预设初始化方法(designated initializer)应该放在第一个,接下来写次要初始化方法(secondary initializers )。

现在有了ARC,几乎不用实现dealloc方法了,但是把deallocinit方法写的近点,从逻辑上来看可以强调他们的成对关系。通常在init方法中做什么了,在dealloc中都要做对应的销毁处理。

init方法的结构应该是这样的:

- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}

为什么我们要把[super init]的返回值赋给self,如果不这么做会怎么样,这是个挺有意思的话题。

到退一步说,我们经常写类似[[NSObject alloc] init]这样的语句,逐渐忽略了allocinit的区别。一个 Objective-C 的特性叫做两步创建。这就意味着分配内存和初始化是两个分开的步骤,因此就需要调用两个不通的方法:allocinit.

-alloc表示为对象分配内存空间。这个过程包括从应用的虚拟内存中为对象分配足够的内存,写入isa指针,初始化retain计数,并把所有实例变量的初值都设为零

-init表示初始化对象,这就意味着把对象转换成可用状态。这通常是指把恰当的初值赋给对象的实例变量。

alloc方法会返回一个未初始化的合法实例对象。每条发送给这个实例的消息都相当于是在调用objc_msgSend()方法,alloc返回对象的指针就指向那个叫做self的参数;这样self就可以调用所有方法了。

为了包含两步创建,通常情况下一个新创建的实例调用的第一个方法都应该是一个init方法。注意在NSObjectinit实现中,什么都没做,只是返回了self而已。

init还有一个重要约定:在调用初始化方法失败的时候init方法会返回nil;初始化可以有很多失败的原因,比如输入的格式有错误,或者必要的对象初始化失败。

这就是为什么我为什么总是调用self = [super init],如果你的父类因为某种原因无法初始化自身,你必须假设自己正处于这种状况下,所以在你的实现中不要还继续去使用初始化返回的nil。如果你没这样做你会得到一个不可用对象,此对象的行为是无法预测的,最终会导致你的 App 崩溃。

重新给self赋值也可以用来让init方法返回不同实例。例子就是 类簇 或者一些返回相同不可变实例对象的 Cocoa 类。

Designated 和 Secondary 初始化方法

Objective-C 有 designated 初始化方法和 secondary 初始化方法的概念。

designated 初始化方法要传入所有参数,如果在调用 designated 初始化方法时仅提供一个或者多个默认参数,那么这种初始化方法就叫做 secondary 初始化方法。

@implementation ZOCEvent
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
location:(CLLocation *)location
{
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
{
return [self initWithTitle:title date:date location:nil];
}
- (instancetype)initWithTitle:(NSString *)title
{
return [self initWithTitle:title date:[NSDate date] location:nil];
}
@end

上面例子里的initWithTitle:date:location:就是 designated 初始化方法,另外两个是 secondary 初始化方法,因为他们只是调用了这个类里已经实现过了的 designated 初始化方法。

Designated 初始化方法

一个类通常有且只有一个 designated 初始化方法,其他的初始化方法都是在调用这个 designated 初始化方法(虽然还是有一种例外情况),这种例外情况并没有要求调用那个初始化函数。

在继承中任何调用 designated 初始化方法都是合法的,而且在继承中得保证所有的 designated 初始化方法都是从先祖(通常是NSObject)到你的类这样,自上而下调用的。

实际上就是说第一个执行初始化代码的是最高级的先祖,然后才轮到继承的类;继承的所有的类都有资格去执行他们特定的初始化代码。总而言之,在开始做实际工作之前你从父类继承的所有东西都是已经是可用的状态。虽然这个状态并不明确,但是所有 Apple 的框架中的类都遵循这个原则,所以你的类也要这样做。

当定义一个新的类时,有三种不同的方法:

第一种办法最简单:你不需要添加任何额外的初始化逻辑,你只需要按照父类的 designated 初始化方法来做。

第二种:当你希望给 designated 初始化方法加入一些其他的初始化逻辑时,你可以重载它。你只需要重载父类的 designated 初始化方法并且保证实现调用了父类的重载方法。

一个经典案例就是当你创建一个 UIViewController的子类时重载initWithNibName:bundle:方法:

@implementation ZOCViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call to the superclass designated initializer
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
@end

UIViewController的子类中重载init方法会报错,因为这种情况下会尝试调用initWithNib:bundle来初始化你的类,你写在init方法里面的实现并不会被调用。这还违背了调用任何 designated 初始化方法的规则。

第三种:如果你想写一个自定义的 designated 初始化函数的时候,你要遵循三点:

很多程序员都忽略了后两步,这不是细心不细心的问题,忽略后两步本身就是违反了框架规范,还会导致一些未知 bug。

看下正确的实现:

@implementation ZOCNewsViewController
- (id)initWithNews:(ZOCNews *)news
{
// call to the immediate superclass's designated initializer
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// Override the immediate superclass's designated initializer
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call the new designated initializer
return [self initWithNews:nil];
}
@end

如果你没重载initWithNibName:bundle方法,正常就只会用initWithNibName:bundle来初始化你的类(这是完全合法的),initWithNews这个方法就永远不会被调用。正是因为你特定的初始化逻辑代码部分并没有被调用,这就导致初始化流程不正确。

即使可以推断那个方法是 designated 初始化方法,但是最好也要明确得说明这是一个 designated 初始化方法(以后你自己或者其他程序员在改这份代码的时候会谢谢你的)。你可以用两种办法(并不相互排斥):

一种是你在文档中就明确出哪一个是 designated 初始化方法,但是你最好是用如下编译器指令来表明你的意图。

__attribute__((objc_designated_initializer))

如果你新的 designated 初始化方法并没有调用父类的 designated 初始化方法,用了这条指令编译器就会提示一条警告。

然而,当没有调用类的 designated 初始化方法(并且提供必要参数)而调用其他父类中的 designated 方法时,会导致当前类处于一个不可用的状态。参考之前的例子,实例化一个ZOCNewsViewController来展示新闻,但是实例化结束后并没有新闻,这就没意义了。这种情况你可以强制只调用一个特殊的 designated 初始化方法,让其他初始化方法都失效。可以用如下编译器指令来修饰这个方法,当你尝试调用这个方法的时候编译器就会报错。

__attribute__((unavailable("Invoke the designated initializer")))

这是上面案例相关实现的头文件(注意用宏来避免代码太罗嗦)

@interface ZOCNewsViewController : UIViewController
- (instancetype)initWithNews:(ZOCNews *)news ZOC_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
- (instancetype)init ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
@end

上述代码要表达的是:永远别从 designated 初始化方法中调用 secondary (如果 secondary 初始化方法遵循规范,他会调用 designated 初始化方法)。如果在 designated 初始化方法中调用了 secondary 初始化方法,很容易会调用到子类重写过的初始化方法,然后就会导致无限递归。

不过一个例外是如果一个对象遵循NSCoding协议,他就用initWithCoder:方法初始化。

我们需要看父类是否遵循NSCoding协议来区别对待。

遵循NSCoding协议的话,直接调用super initWithCoder:可能会和 designated 初始化方法有些共享代码。解决这个问题的一个好办法就是写在一个私有方法里(比如p_commonInit)。

当父类不遵循NSCoding协议,建议把initWithCoder:当做 secondary 初始化方法来看,因此就要调用 self的 designated 初始化方法。注意,这跟 Apple 在 Archives and Serializations Programming Guide 中建议的如下规范相违背:

the object should first invoke its superclass’s designated initializer to initialize inherited state

对象应该先调用父类的 designated 初始化方法来初始化继承状态

依据此原则,如果你的类不是NSObject的直接子类,这样做就会造成未知隐患。

次要初始化方法

就如上面一段所说的,secondary 初始化方法是一种非常便捷为 designated 初始化方法提供默认值或默认动作的方法。也就是说,在 secondary 初始化方法里不应该有任何强制的出初始化方法,而且要假设这个方法永远不被直接调用。重申一次,我们得保证直接被调用的是 designated 初始化方法。

这就意味着,你的 designated 初始化方法应该是调用其他 secondary 初始化方法或者你self的 designated 初始化方法。有时候可能不小心了,写成了super,这样就会导致并未遵循上述的顺序来初始化(在这种特殊的情况里,跳过了当前类的初始化)。

参考

instancetype

我们总是忘记 Cocoa 是充满各种规范的,这些规范可以让编译器变的更聪明一些。无论编译器是否碰到alloc或者init方法,他都知道这两个方法返回的类型都是id,那些方法总是返回接受到类的实例对象。因此,这样就为编译器进行类型检查提供了可能(比如,检查方法返回类型是否合法)。Clang 的这个好处来自于 related result type,意味着:

messages sent to one of alloc and init methods will have the same static type as the instance of the receiver class

alloc 和 init 方法会检查发送来的消息返回的静态类型和接受到的类实例类型是否相同

想获得更多关于自动定义返回类型的相关规范清参考 Clang Language Extensions guide 中的 appropriate section

instancetype关键字作为返回类型可以让相关返回结果类型更加明确,在一些工厂方法或者构造器方法这些场景下很有用。这可以提醒编译器检查类型是否正确,更重要的是,子类的类型是否正确也会检查。

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name;
@end

尽管如此,根据 clang 规定,编译器可以把id升级到instancetype。在alloc或者init中,我们强烈推荐对所有返回类实例的实例化方法和类方法,都用instancetype作为返回类型。

在你所有的 API 中,你最应该建立起一种习惯来保持统一性(可能还会增加可读性)。此外,通过对代码的小调整可以增加你代码的可读性:简单浏览一下你的代码就能分辨出哪些方法是返回当前类的实例。以后你会感谢在意过这些细节。

参考

初始化模式

类簇(Class cluster)

在 Apple 的文档中对类簇的描述是这样:

an architecture that groups a number of private, concrete subclasses under a public, abstract superclass.

类簇是,在一个公共的、抽象的父类下,一组私有的、具体的子类架构。

如果这个描述你听着耳熟,那就对了。类簇其实就是 Apple 语言体系中的抽象工厂设计模式。

类簇的主旨很简单:一个抽象的父类,在初初始化过程中处理信息,经常为初始化方法提供参数或者从环境中获取,来实现逻辑并初始化一个具体的子类。这个”公共面向(public facing)”的类应该对自己的子类有所掌控,并返回最优类型的私有子类。

这个模式非常有用,因为这让调用者省去了繁杂的初始化逻辑,只需要知道要通信对象的接口,不用关心具体的内部实现。

Apple 的框架中广泛使用了类簇;一些值得注意的例子有NSNumber,它可以根据提供的数字类型(Integer,Float,etc…)来返回对应的子类,还有NSArray会根据最优存储策略返回最适合的具体子类。

这个模式的优点在于,调用者可以完全不用关心子类;实际上可以用来设计一个库,用来切换实际返回的类,同时还不必暴露任何实现的细节,因为他们都遵循抽象父类的方法。

依我们的经验来看用类簇可以对移除一堆条件语句很有帮助。

一个典型的例子就是当你在 iPhone 和 iPad 中都有 UIViewController 的子类,但是在不同的设备上有不同的表现。

基础的实现办法是在方法中用一些条件语句来检查设备,然后执行不同的逻辑。虽然开始的时候这部分条件逻辑语句只有几行,但是随着要处理的步骤增多代码也就越来越冗杂了。

一个更好的实现设计是创建一个抽象且通用的 view controller 包含所有共享逻辑,并对应不同设备有两个特别的子类。

这个通用的 view controller 会检查当前设备的特性并根据这一特性返回对应的子类。

@implementation ZOCKintsugiPhotoViewController
- (id)initWithPhotos:(NSArray *)photos
{
if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
self = nil;
if ([UIDevice isPad]) {
self = [[ZOCKintsugiPhotoViewController-iPad alloc] initWithPhotos:photos];
}
else {
self = [[ZOCKintsugiPhotoViewController-iPhone alloc] initWithPhotos:photos];
}
return self;
}
return [super initWithNibName:nil bundle:nil];
}
@end

上述的代码例子展示如何创建一个类簇。首先下述语句防止子类重载初始化方法,避免了无限递归。

[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]

当下述代码被调用时,上述检测会为 true,self = nil是用来移除所有对ZOCKintsugiPhotoViewController 实例的引用,这样他就会被释放,按照逻辑来决定初始化哪个子类。

[[ZOCKintsugiPhotoViewController alloc] initWithPhotos:photos]

我们假设在 iPhone 上跑这段代码ZOCKintsugiPhotoViewController-iPhone没有重载initWithPhotos:;这时候,执行

self = [[ZOCKintsugiPhotoViewController-iPhone alloc] initWithPhotos:photos];

ZOCKintsugiPhotoViewController将会被调用,并当第一次检查时ZOCKintsugiPhotoViewController 的类检查将会是 false,那么就会直接调用

return [super initWithNibName:nil bundle:nil];

这样就会按照之前重点讲过的初始化方法来继续进行初始化。

单例(Singleton)

能不用就别用单例,用依赖注入来代替。

然而,一定要用单例用该用一个安全线程模式来创建共享实例。对于 GCD,可以用dispatch_once()函数。

+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

用同步的dispatch_once()来代替下方旧的老办法:

+ (instancetype)sharedInstance
{
static id sharedInstance;
@synchronized(self) {
if (sharedInstance == nil) {
sharedInstance = [[MyClass alloc] init];
}
}
return sharedInstance;
}

相比上述例子dispatch_once()的优势在于更快,语意上也更明确,因为dispatch_once()的总体含义就是”执行且只执行一次”,正好和我们做的一样。这样同时可以避免possible and sometimes prolific crashes

使用单例对象的经典例子是 GPS 和设备加速器。尽管单例对象可以子类化,但是在这些情况下也是挺好用的。这个接口应该说明,给出的类应该用单例模式。因此,通常一个单独的公共sharedInstance类方法就够了,并且只读的属性也应该暴露。

在代码或者层之间,把单例用作一个对象容器来共享是很傻很糟糕的,是一个不好的设计。

属性(Properties)

属性应该尽量使用描述性命名,避免缩写,而且要用以小写字母开头的驼峰式。幸运的是我们选择的工具可以帮我们自动补全(呃…几乎是所有的,嗯,我说的就是 Xcode 的 Derived Data),所以没理由去节省一些字符,尽可能在你的源码里表达更多的信息。

例如:

NSString *text;

不要这样:

NSString* text;
NSString * text;

(注意:这个习惯和常量不同。这确实是为常用和可读性考虑。C++ 开发者更喜欢把变量的类型从名字中分离出来,作为一个纯粹的类型就应该是NSString*(这是对于从堆中分配的对象,C++ 应该是从栈上分配对象)。那就用该用NSString* text格式来写。)

用属性的 auto-synthesize,别用手写的@synthesize,除非你的属性只是 protocol 的一部分而不是一个具体的类。如果 Xcode 能自动同步变量,那就让 Xcode 做。否则你就是放弃了 Xcode 的优势来维护一段很冗余的代码。

除了 initdealloc方法,应该经常使用 set 和 get 方法访问属性。通常来说,用属性来访问当前作用域以外的的代码增加了一些视觉上的便利,但是也会存在副作用。

用 setter 的好处有:

  • 使用 setter 会遵循内存管理的定义(strong, weak, copy 等等…)。这在 ARC 之前就挺重要的,有了 ARC 之后依旧也很重要。举个例子,copy的语意:每次你使用 setter 传入的值都是一个副本,没有任何额外的操作。
  • KVO 通知(willChangeValueForKey, didChangeValueForKey) 会被自动执行。
  • 更容易debug:你可以在属性声明上设置一个断点,setter/getter 每次执行时断点就会生效,或者你也可以在自定义的 setter/getter 设置断点
  • 在设定值的时候可以增加额外的逻辑

用 getter 的好处:

  • 扩展性和适应性更好(比如:属性是自动生成的)
  • 允许子类化
  • 更容易 debug (比如:可以在 getter 中打一个断点来看看到底是谁调用了这个特殊的getter)
  • 让目的更清晰明确:在访问一个 ivar _anIvar时实际上你是在访问self->_anIvar。这可能会导致一些问题,比如,在一个 block 内访问 iVar(尽管你并没有明确的看到self关键字,但是你还是 retain 了 self)
  • 自动产生 KVO 通知
  • 发消息的额外性能开销很低,大部分情况下是可以忽略不计的。更多关于属性的性能问题介绍可以参考Should I Use a Property or an Instance Variable?

Init 和 Dealloc

但是有个一个例外:在init(和其他初始化方法)中你千万不能用 setter (或者 getter),你应该直接访问实例变量。这是为了防止在子类化的时候出现问题:实际上一个子类可以重载 setter (或者 getter)取调用其他方法,访问属性或者 iVar 时他们可能并未完全初始化。要记住,只有在 init 返回时,一个对象才被认为是完全初始化完成状态。在 dealloc 方法(在 dealloc方法中对象可能在一个不确定的状态)这些也是一样的。

下述文档中也都反复陈述很多次了:

此外,在 init 中使用 setter 不会很好的执行 UIAppearence代理(更多信息参见 UIAppearance for Custom Views )。

点语法

当在使用 setter/getter 时更倾向于用点语法,在设置属性时通常都应该用点语法。

例如:

view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;

不要:

[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;

使用点语法会让表达更清晰,并且帮助区分是在访问属性还是在调用方法。

属性声明

按照如下格式来声明属性

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

属性参数应该按照如下顺序:原子性,是否可读写,内存管理。这样写你在修改属性的时候就能很容易的找到位置并且看起来更方便。

除非特殊必要,应该都要使用nonatomic,在 iOS 中,atomic锁会非常影响性能。

属性可以存储一个 block,想让其生命周期贯穿定义域,应该用copy来修饰(block 创建时在栈中,用copy可以让其拷贝到堆中)。

为了实现一个共有的 getter 和一个私有的 setter,你应该用readonly来声明属性,并在类扩展中重新将其声明为readwrite

@interface MyClass : NSObject
@property (nonatomic, readonly) NSObject *object
@end
@implementation MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object
@end

如果一个Bool类型的属性的命名表达了描述性含义,这个属性可以省略”is”,但是在 get 方法访问时依照惯例还是要加上特定的”is”,例如:

@property (assign, getter=isEditable) BOOL editable;

正文和举例引用自Cocoa Naming Guidelines.

在实现文件中就不必再用@synthesize,Xcode 已经帮你加好了。

私有属性

私有属性应该在类实现文件中的类扩展(class extensions,即没有名字的 categories)中声明。除非要扩展其他的类,否则不要使用有名称的 categories (例如,ZOCPrivate)。

For example:

@interface ZOCViewController ()
@property (nonatomic, strong) UIView *bannerView;
@end

可变对象

潜在可以被可变对象(例如,NSStringNSArrayNSURLRequest)赋值的任何属性,其内存管理类型必须为copy。这是为了保证封装的安全,同时避免在对象不知情的情况下,修改了属性的值。

在公共接口中也应该避免暴露可变对象,因为这个类的使用者就可以改变这个类的内部描述同时破坏了封装。可以提供一个只读属性,返回对象的不可变副本:

/* .h */
@property (nonatomic, readonly) NSArray *elements
/* .m */
- (NSArray *)elements {
return [self.mutableElements copy];
}

懒加载

实例化一个对象是很消耗资源的,很多情况下在实例化的过程中包括了一些设置,但是又不想弄乱调用的方法,并且这种设置只需要一次。

在这种情况下,可以选择重载属性 getter 来懒初始化,以此来代替在初始化方法中实例化对象。通常这类操作的案例如下:

- (NSDateFormatter *)dateFormatter {
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setLocale:enUSPOSIXLocale];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSSS"];
}
return _dateFormatter;
}

尽管这样在某些特定情况下挺好的,但是在用之前还是要深思熟虑的,实际上这中行为是应该规避的。下列是使用懒加载的一些反对论点:

  • getter 理论上是没有副作用的。从另一个角度看你不应该觉得 getter 会实例化一个对象或者觉得这是产生副作用了。实际上,尝试去调用一个没有返回值的 getter 编译器会警告:“getter 不会产生副作用;”。
  • 在首次访问时把初始化的损耗作为副作用移除了,这会导致性能优化问题(也很难进行测试)。
  • 初始化的时刻可能是不确定的:例如,你预计这个属性是被某个方法首次访问,但是你后来改变了类的实现,导致访问器被调用的时刻比你预期的要早。这会导致很多问题,尤其是如果初始化逻辑依赖于该类的其他状态时,那结果就不同了。总而言之最好明确依赖关系。
  • 这种行为对 KVO 不友好。如果 getter 改变了引用它应该通过 KVO 通知来通知改变,当访问 getter 就会得到一个改变通知这种方法就挺蹩脚的。

方法

参数断言

你的方法可能要求一些参数来满足特定的条件(比如不能为 nil);这种状况下最好用NSParameterAssert()来判断条件是否成立或者抛出异常。

私有方法

永远不要在你的私有方法前加上下划线_,这个前缀是 Apple 保有的,这样做会让你面临重载 Apple 已有私有方法的险境。

相等

当你要实现相等比较时,要记住这个约定:你要同时实现isEqualhash方法。如果通过isEqual认为两个对象是相等的,hash方法必须返回相同值,但是如果hash方法返回相同值,并不能认为两个对象是等值的。

这个约定归根究底就是,在对象存储在集合(例如,NSDictionaryNSSet在底层使用 hash 表的数据结构)中时,如何查找他们。

@implementation ZOCPerson
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[ZOCPerson class]]) {
return NO;
}
// check objects properties (name and birthday) for equality
...
return propertiesMatch;
}
- (NSUInteger)hash {
return [self.name hash] ^ [self.birthday hash];
}
@end

一定要注意的是 hash 方法一定不能返回一个常量。这是一个典型的错误并且会导致很严重的问题,因为当 hash 方法返回值用作 hash 表的 key 时,会导致 hash 表的100%碰撞。

你通常应该按照isEqualTo<#class-name-without-prefix#>这种格式来实现是否相等的检测方法。通常应该优先调用这个验证相等方法来避免上述的类型检测方法。

一个完整的 isEqueal* 方法应该是如下样式的:

- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[ZOCPerson class]]) {
return NO;
}
return [self isEqualToPerson:(ZOCPerson *)object];
}
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL namesMatch = (!self.name && !person.name) ||
[self.name isEqualToString:person.name];
BOOL birthdaysMatch = (!self.birthday && !person.birthday) ||
[self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}

如果一个给定的对象实例被加入到一个容器对象(如,NSArrayNSSet或者NSDictionary),其 hash计算结果应该是确定的,否则加入容器对象的行为将无法定义(所有容器对象都是用对象的 hash 值来进行查找行为的,或者实现一些特殊属性,比如包含对象的唯一性检测)。也就是说,最好只用不可变属性来计算 hash 值,或者,最好保证对象是不可变的。