理解“属性”这一概念
- 可以用@property来定义对象中所封装的数据。
@interface MSTPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
- 通过“特质”来制定存储数据所需的正确语义。
@property (nonatomic, readwrite, copy) NSString *firstName;
- 原子性:
- atomic 通过锁定机制。
- nonatomic 不加锁。
- 读/写权限:
- readwrite 拥有"getter"(获取方法)和"setter"(设置方法)。
- readonly 只会生成"getter"。
- 内存管理语义:
- assign 只针对“纯量类型”(scalar type),就是C语言中的一些变量。
- strong 定义了一种“拥有关系”(owning relationship),在ARC中会是retain count +1。
- weak 定义了一种“非拥有关系”(nonowning relationship)。设置方法既不保留新值,并释放旧值,在属性所指的对象遭到摧毁时,属性值会清空。
- unsafe_unretained 此特质的语义和assign相同,但是它是适用于“对象类型”(object type), 但当属性所指的对象遭到摧毁时,属性值不会清空。
- copy 于strong类似设置方法不保留新值,而是拷贝。一般NSString经常用此特质。但是拷贝都是不可变的。
- 方法名:
- getter=
指定“getter”方法名。
- getter=
@property (nonatomic, getter=isOn) BOOL on;
- setter=
指定“setter”方法名。但是这种用法不常见。 - 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序是应该使用nonatomic属性,因为atomic属性会严重影响性能。
在对象内部尽量直接访问实例变量
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 这两种写法有几个区别:
- 由于不经过Objective-C的“方法派发”(method dispatch)步骤,所以直接访问实例变量的速度比较快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
- 直接访问实例变量,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
- 如果直接访问实例变量,那么不会触发“键值观测”(KVO)通知。这样做是否会产生问题,还取决于具体的对象行为。
- 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”或“设置方法”中新增“断点”(breakpoint),监控该属性的调用者及其访问时机。
- 这两种写法有几个区别:
- 在初始化访问及dealloc方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化(lazy initialization)技术配置某份数据,这种情况下,需要通过属性来读取数据。
- (MSTCar *)myCar{
if (!_myCar) {
_myCar = [MSTCar alloc] init];
}
return _myCar;
}
理解“对象等同性”(equality)这一概念
- 若想检测对象的等同性,请提供“isEqual:”与hash方法。
NSString *strA = @"Mustard 123";
NSString *strB = [NSString stringWithFormat:@"Mustard %i", 123];
BOOL equalA = (strA == strB); //NO
BOOL equalB = [strA isEqual:strB]; //YES
BOOL equalC = [strA isEqualToString:strB]; //YES
NSString有一个独有的方法"isEqualToString:",同样的NSArray和NSDictionary也有类似的方法:"isEqualToArray"和"isEqualToDictionary"。调用本类的等同性判断方法相对于"isEqual:"更快一点,因为后者还需要判断受测对象的类型是否一致。
相同对象必须具有相同的hash code(哈希码),但是两个hash code相同的对象未必是相同的。
不要盲目地逐个检测每条属性,而是应该依照具体需求来制定监测方案。
举个简单的例子,每个MSTPerson类都有四个属性:firstName、lastName、age、identifier。现在需要判断两个person是否相等,只需要判断identifier是否相等,而不需要进行“深度等同性判定”(deep equality,就是比较每个属性是否相等)。
- 编写hash方法时,应该使用计算速度快而且hash code碰撞几率低的算法。
以“类族模式”(class cluster pattern)隐藏实现细节
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
比如UIKit框架中的UIButton类。想创建一个button,需要调用下面这个“类方法”(class method):
+ (UIButton *)buttonWithType:(UIButtonType)type;
这个方法返回的对象取决于用户传入的button type。然而,不管返回的是什么类型的button,都是继承于同一个基类:UIButton。这么做的意义就是,使用者不需要关心创建出来的button具体是哪个子类,也不需要去考虑button绘制方式等实现细节。使用者这需要明白如何设置"title"这样的属性就可以了。
但是回过头来说,也可以这样写:
- (void)drawRect:(CGRect)rect {
if (_type == TypeA) {
//Draw TypeA button
} else if (_type == TypeB) {
//Draw TypeB button
} /* ... */
}
这样看上去还算简单,但是需要依照button type来切换的绘制方法会有许多种,这样就变得特别的麻烦。所以应该将这种代码重构为多个子类,把各种button所用到的绘制方法放到相关的子类中去。但是这么做有一个小的弊端,就是使用者需要知道各种子类才行。虽然这样,但是这个小的弊端几乎可以说是忽略不计。
这里有一点需要注意的是:在创建实例的时候,你可能觉得自己创建的是某个类的实例,然而实际上创建的确实其子类的实例。比如下面这段代码:
typedef NS_ENUM(NSUInteger, MSTEmployeeType) {
MSTEmployeeTypeDeveloper,
MSTEmployeeTypeDesigner,
MSTEmployeeTypeFinance
};
@inerface MSTEmployee : NSObject
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSUInteger salary;
//用来创建Employee实例
+ (MSTEmployee *)employeeWithType:(MSTEmployeeType)type;
//Employee每天的工作
- (void)doADayWork;
@end
@implementation MSTEmployee
+ (MSTEmployee *)employeeWithType:(MSTEmployeeType)type {
switch (type) {
case MSTEmployeeTypeDeveloper:
return [[MSTEmployeeDeveloper alloc] init];
break;
case MSTEmployeeTypeDesigner:
return [[MSTEmployeeDesigner alloc] init];
break;
case MSTEmployeeTypeFinance:
return [[MSTEmployeeFinance alloc] init];
break;
}
}
- (void)doADayWork {
//子类来实现具体的工作
}
@end
在上面这个例子中,[employee isMemberOfClass:[MSTEmployee class]]似乎返回的是YES,但是实际上返回的趋势NO,因为employee并非Employee类的实例,而是其某个子类的实例。
- 系统框架中经常会使用类族。
像上面所说的UIButton就是一个例子。大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。
在类族中实现子类是需要遵循的规范,一般都会定义于基类的文档中,编码前应该先看看。下面,列出了新增子类的时候需要遵守几条规则:
- 子类应该继承自类族中的抽象基类。
若要编写NSArray类族的子类,则需令其继承自不可变数组的基类或可变数组的基类。
- 子类应该定义自己的数据储存方式。
子类必须用一个实例变量来存放数组中的对象。NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。
- 子类应当覆写超类文档中指明需要覆写的方法。
每个抽象基类中,都有一些子类必须覆写的方法。:想要编写NSArray的子类,就需要实现count及"objectAtIndex:"方法。
在既有类中使用关联对象存放自定义数据
- 可以通过“关联对象”机制来把两个对象连起来:
//此方法以给定的键和策略为某对象设置关联对象值
void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy)
//此方法以给定的键从对象中获取相应的关联对象值
id objc_getAssociatedObject (id object, void *key)
//此方法移除指定对象的全部关联对象
void objc_removeAssociatedObjects (id object)
我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条目。于是,存取关联对象的值就相当于在NSDictionary对象调用[object setObject:value forKey:key]与[object objectForKey:key]方法。但是,两者之间有个重要差别:设置关联对象是用的键(key)是个“不透明的指针”(opaque pointer)。在设置关联对象值时,通常使用静态全局变量做键。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性是所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。