属性
属性应该尽可能描述性地命名,避免缩写,并且是小写字母开头的驼峰命名。我们的工具可以很方便地帮我们自动补全所有东西(嗯。。几乎所有的,Xcode 的 Derived Data 会索引这些命名)。所以没理由少打几个字符了,并且最好尽可能在你源码里表达更多东西。
例子 :
NSString *text;
不要这样 :
NSString* text;
NSString * text;
(注意:这个习惯和常量不同,这是主要从常用和可读性考虑。 C++ 的开发者偏好从变量名中分离类型,作为类型它应该是NSString*
(对于从堆中分配的对象,对于C++是能从栈上分配的)格式。)
使用属性的自动同步 (synthesize) 而不是手动的 @synthesize
语句,除非你的属性是 protocol 的一部分而不是一个完整的类。如果 Xcode 可以自动同步这些变量,就让它来做吧。否则只会让你抛开 Xcode 的优点,维护更冗长的代码。
你应该总是使用 setter 和 getter 方法访问属性,除了 init
和 dealloc
方法。通常,使用属性让你增加了在当前作用域之外的代码块的可能所以可能带来更多副作用。
你总应该用 getter 和 setter ,因为:
- 使用 setter 会遵守定义的内存管理语义(
strong
,weak
,copy
etc…) ,这个在 ARC 之前就是相关的内容。举个例子,copy
属性定义了每个时候你用 setter 并且传送数据的时候,它会复制数据而不用额外的操作。 - KVO 通知(
willChangeValueForKey
,didChangeValueForKey
) 会被自动执行。 - 更容易debug:你可以设置一个断点在属性声明上并且断点会在每次 getter / setter 方法调用的时候执行,或者你可以在自己的自定义 setter/getter 设置断点。
- 允许在一个单独的地方为设置值添加额外的逻辑。
你应该倾向于用 getter:
- 它是对未来的变化有扩展能力的(比如,属性是自动生成的)。
- 它允许子类化。
- 更简单的debug(比如,允许拿出一个断点在 getter 方法里面,并且看谁访问了特别的 getter
- 它让意图更加清晰和明确:通过访问 ivar
_anIvar
你可以明确的访问self->_anIvar
.这可能导致问题。在 block 里面访问 ivar (你捕捉并且 retain 了 self,即使你没有明确的看到 self 关键词)。 - 它自动产生KVO 通知。
- 在消息发送的时候增加的开销是微不足道的。更多关于性能问题的介绍你可以看 Should I Use a Property or an Instance Variable?。
Init 和 Dealloc
有一个例外:永远不要在 init 方法(以及其他初始化方法)里面用 getter 和 setter 方法,你应当直接访问实例变量。这样做是为了防止有子类时,出现这样的情况:它的子类最终重载了其 setter 或者 getter 方法,因此导致该子类去调用其他的方法、访问那些处于不稳定状态,或者称为没有初始化完成的属性或者 ivar 。记住一个对象仅仅在 init 返回的时候,才会被认为是达到了初始化完成的状态。
同样在 dealloc 方法中(在 dealloc 方法中,一个对象可以在一个 不确定的状态中)这是同样需要被注意的。
- Advanced Memory Management Programming Guide under the self-explanatory section “Don’t Use Accessor Methods in Initializer Methods and dealloc”;
- Migrating to Modern Objective-C at WWDC 2012 at slide 27;
- in a pull request form Dave DeLong’s.
此外,在 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
带来的锁特别影响性能。
属性可以存储一个代码块。为了让它存活到定义的块的结束,必须使用 copy
(block 最早在栈里面创建,使用 copy
让 block 拷贝到堆里面去)
为了完成一个共有的 getter 和一个私有的 setter,你应该声明公开的属性为 readonly
并且在类扩展总重新定义通用的属性为 readwrite
的。
//.h文件中
@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object;
@end
//.m文件中
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object;
@end
@implementation MyClass
//Do Something cool
@end
描述BOOL
属性的词如果是形容词,那么setter不应该带is
前缀,但它对应的 getter 访问器应该带上这个前缀,如:
@property (assign, getter=isEditable) BOOL editable;
文字和例子引用自 Cocoa Naming Guidelines。
在实现文件中应避免使用@synthesize
,因为Xcode已经自动为你添加了。
私有属性
私有属性应该定义在类的实现文件的类的扩展 (class extensions) 中。不允许在有名字的的 category(如 ZOCPrivate
)中定义私有属性,除非你扩展其他类。
例子:
@interface ZOCViewController ()
@property (nonatomic, strong) UIView *bannerView;
@end
可变对象
任何可以用来用一个可变的对象设置的((比如 NSString
,NSArray
,NSURLRequest
))属性的的内存管理类型必须是 copy
的。
这是为了确保防止在不明确的情况下修改被封装好的对象的值(译者注:比如执行 array(定义为 copy 的 NSArray 实例) = mutableArray,copy 属性会让 array 的 setter 方法为 array = [mutableArray copy], [mutableArray copy] 返回的是不可变的 NSArray 实例,就保证了正确性。用其他属性修饰符修饰,容易在直接赋值的时候,array 指向的是 NSMuatbleArray 的实例,在之后可以随意改变它的值,就容易出错)。
你应该同时避免暴露在公开的接口中可变的对象,因为这允许你的类的使用者改变类自己的内部表示并且破坏类的封装。你可以提供可以只读的属性来返回你对象的不可变的副本。
/* .h */
@property (nonatomic, readonly) NSArray *elements
/* .m */
- (NSArray *)elements {
return [self.mutableElements copy];
}
懒加载(Lazy Loading)
当实例化一个对象需要耗费很多资源,或者配置一次就要调用很多配置相关的方法而你又不想弄乱这些方法时,我们需要重写 getter 方法以延迟实例化,而不是在 init 方法里给对象分配内存。通常这种操作使用下面这样的模板:
- (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.SSS"];//毫秒是SSS,而非SSSSS
}
return _dateFormatter;
}
即使这样做在某些情况下很不错,但是在实际这样做之前应当深思熟虑。事实上,这样的做法是可以避免的。下面是使用延迟实例化的争议。
- getter 方法应该避免副作用。看到 getter 方法的时候,你不会想到会因此创建一个对象或导致副作用,实际上如果调用 getter 方法而不使用其返回值编译器会报警告 “Getter 不应该仅因它产生的副作用而被调用”。
副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。(译者注)
- 你在第一次访问的时候改变了初始化的消耗,产生了副作用,这会让优化性能变得困难(以及测试)
- 这个初始化可能是不确定的:比如你期望属性第一次被一个方法访问,但是你改变了类的实现,访问器在你预期之前就得到了调用,这样可以导致问题,特别是初始化逻辑可能依赖于类的其他不同状态的时候。总的来说最好明确依赖关系。
- 这个行为不是 KVO 友好的。如果 getter 改变了引用,他应该通过一个 KVO 通知来通知改变。当访问 getter 的时候收到一个改变的通知很奇怪。