编程规范

本篇博文是在学习Zen and the Art of the Objective-C Craftsmanship时的笔记。这本书的观点是代码不仅是可以编译的,同时应该是 “有效” 的。好的代码有一些特性:简明,自我解释,优秀的组织,良好的文档,良好的命名,优秀的设计以及可以被久经考验。这本书的一个理念是是代码的清晰性优先于性能,同时阐述为什么应该这么做。

if

推荐

1
2
3
if (!error) {
return success;
}

不推荐

1
2
if (!error)
return success;
1
if (!error) return success;

尤达表达式

不要使用尤达表达式。尤达表达式是指,拿一个常量去和变量比较而不是拿变量去和常量比较。它就像是在表达 “蓝色是不是天空的颜色” 或者 “高个是不是这个男人的属性” 而不是 “天空是不是蓝的” 或者 “这个男人是不是高个子的”

推荐

1
if ([myValue isEqual:@42]) { ...

不推荐

1
if ([@42 isEqual:myValue]) { ...

nil和BOOL检查

容易出现Yoda,如:

1
if (nil == myValue) { ...

而不是

1
if (myValue == nil) { ...

避免

1
if (myValue = nil) { ...

推荐

1
2
3
if (someObject) { ...
if (![someObject boolValue]) { ...
if (!someObject) { ...

不推荐

1
2
3
if (someObject == YES) { ... // Wrong
if (myRawValue == YES) { ... // Never do this.
if ([someObject boolValue] == NO) { ...

黄金大道

使用多个 return 可以避免增加循环的复杂度,并提高代码的可读性。因为方法的重要部分没有嵌套在分支里面,并且你可以很清楚地找到相关的代码。

推荐

1
2
3
4
5
6
7
- (void)someMethod {
if (![someOther boolValue]) {
return;
}

//Do something important
}

不推荐

1
2
3
4
5
- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}

Switch

  1. 如果一个case包含多行语句,建议加上括号
  2. 有时候可以使用 fall-through 在不同的 case 里面执行同一段代码。一个 fall-through 是指移除 case 语句的 “break” 然后让下面的 case 继续执行。


1
2
3
4
5
6
7
8
9
switch (condition) {
case 1:
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}

命名

通用

用较长的、描述性的方法和变量名。

常量

以驼峰法命名,并以相关类名作为前缀。

推荐

1
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;

不推荐

1
static const NSTimeInterval fadeOutTime = 0.4;

推荐使用常量来代替字符串字面值和数字,这样能够方便复用,而且可以快速修改而不需要查找和替换。常量应该用 static 声明为静态常量,而不要用 #define,除非它明确的作为一个宏来使用。

区别:

define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
define不做检查,不会报编译错误,只是替换。
const会编译检查,会报编译错误。
define在展开的时候才分配内存,展开几次分配几次内存。
const在定义的时候会分配一次内存到静态区,使用时不重复分配。
define可以定义一些简单的运算函数。

举例

  1. 定义一个字符串常量:
1
2
3
4
5
> #define NAME @"henry"
> static NSString *const name = @"henry";
> 两个均可使用,因为OC中的字符串常量存储在静态区,即相同的字符串常量在内存中只会存储一份。
> 但是苹果的代码中较多使用的是const
>
  1. 使用define定义简单的函数
1
2
3
> // rgb颜色转换(16进制->10进制)
> #define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
>

常量应该在头文件中以这样的形式暴露给外部:extern NSString *const ZOCCacheControllerDidClearCacheNotification; 并在实现文件中为它赋值。

方法

方法段之间以空格间隔,参数前加一个描述性的关键词,尽量少用and

推荐

1
2
3
4
- (void)setExampleText:(NSString *)text image:(UIImage *)image;
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
- (id)viewWithTag:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不推荐

1
2
3
4
5
- (void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height; // Never do this.

类名

类名应该以三个大写字母作为前缀(双字母前缀为 Apple 的类预留)。尽管这个规范看起来有些古怪,但是这样做可以减少 Objective-C 没有命名空间所带来的问题。

Initializer 和 dealloc

推荐的代码组织方式是将 dealloc 方法放在实现文件的最前面(直接在 @synthesize 以及 @dynamic 之后),init 应该跟在 dealloc 方法后面。

如果有多个初始化方法, 指定初始化方法 (designated initializer) 应该放在最前面,间接初始化方法 (secondary initializer) 跟在后面,这样更有逻辑性。如今有了 ARC,dealloc 方法几乎不需要实现,不过把 init 和 dealloc 放在一起可以从视觉上强调它们是一对的。通常,在 init 方法中做的事情需要在 dealloc 方法中撤销。

Designated 和 Secondary 初始化方法

Objective-C 有指定初始化方法(designated initializer)和间接(secondary initializer)初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化的初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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

  1. 应该永远不从 designated initializer 里面调用一个 secondary initializer (如果secondary initializer 遵守约定,它会调用 designated initializer)。如果这样,调用很可能会调用一个子类重写的 init 方法并且陷入无限递归之中。
  2. 对象总是应该首先调用超类的 designated initializer 来初始化继承的状态
  3. secondary initializer 是一种提供默认值、行为到 designated initializer的方法。也就是说,在这样的方法里面你不应该有初始化实例变量的操作,并且你应该一直假设这个方法不会得到调用。我们保证的是唯一被调用的方法是 designated initializer。

instancetype

一个相关的返回类型可以明确地规定用 instancetype 关键字作为返回类型,并且它可以在一些工厂方法或者构造器方法的场景下很有用。它可以提示编译器正确地检查类型,并且更加重要的是,这同时适用于它的子类。

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

初始化模式

类簇

From Apple:

an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一个在共有的抽象超类下设置一组私有子类的架构)

Class cluster 是 Apple 对抽象工厂设计模式的称呼。

我们的经验是使用类簇可以帮助移除很多条件语句。

一个经典的例子是如果你有为 iPad 和 iPhone 写的一样的 UIViewController 子类,但是在不同的设备上有不同的行为。

比较基础的实现是用条件语句检查设备,然后执行不同的逻辑。虽然刚开始可能不错,但是随着代码的增长,运行逻辑也会趋于复杂。 一个更好的实现的设计是创建一个抽象而且宽泛的 view controller 来包含所有的共享逻辑,并且对于不同设备有两个特别的子例。

通用的 view controller 会检查当前设备并且返回适当的子类。

ZOCKintsugiPhotoViewController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

- (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

单例

如果可能,请尽量避免使用单例而是依赖注入。 然而,如果一定要用,请使用一个线程安全的模式来创建共享的实例。对于 GCD,用 dispatch_once() 函数就可以咯。

1
2
3
4
5
6
7
8
9
+ (instancetype)sharedInstance
{

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

使用 dispatch_once(),来控制代码同步,取代了原来的约定俗成的用法。

1
2
3
4
5
6
7
8
9
10
+ (instancetype)sharedInstance
{

static id sharedInstance;
@synchronized(self) {

if (sharedInstance == nil) {
sharedInstance = [[MyClass alloc] init];
}
}
return sharedInstance;
}

dispatch_once() 的优点是,它更快,而且语法上更干净,因为dispatch_once()的意思就是 “把一些东西执行一次”,就像我们做的一样。 这样同时可以避免一些线程不安全的问题(详见讨论here

单例通常公开一个sharedInstance的类方法就已经足够了,没有任何的可写属性需要被暴露出来。
尝试着把单例作为一个对象的容器,在代码或者应用层面上共享,是一个糟糕和丑陋的设计。

属性

推荐

1
NSString *text;

不推荐

1
2
NSString * text;
NSString* text;

Init 和 Dealloc

有一个例外:永远不要在 init 方法(以及其他初始化方法)里面用 getter 和 setter 方法,你应当直接访问实例变量。这样做是为了防止有子类时,出现这样的情况:它的子类最终重载了其 setter 或者 getter 方法,因此导致该子类去调用其他的方法、访问那些处于不稳定状态,或者称为没有初始化完成的属性或者 ivar 。记住一个对象仅仅在 init 返回的时候,才会被认为是达到了初始化完成的状态。

换一种说法,在 init 和 dealloc 中,对象的存在与否还不确定,所以给对象发消息可能不会成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

@property(nonatomic,strong)NSArray *topArray;


- (instancetype)initWithData:(NSMutableArray *)dArray{

if (self = [super init]) {
//建议使用
_topArray=[NSMutableArray arrayWithArray:dArray];
//不建议使用
self.topArray=[NSMutableArray arrayWithArray:dArray];
}

return self;
}

id还是instancetype

ARC 中关于属性的修饰符

atomic, nonatomic

atomic:默认是有该属性的,这个属性是为了保证程序在多线程情况,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题。

nonatomic:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率。

你必须使用 nonatomic,除非特别需要的情况。在iOS中,atomic带来的锁特别影响性能。

readwrite, readonly

readwrite:这个属性是默认的情况,会自动为你生成存取器。

readonly:只生成getter不会有setter方法。

readwrite、readonly这两个属性的真正价值,不是提供成员变量访问接口,而是控制成员变量的访问权限。

strong, weak

strong 修饰的属性会在赋值时调用被指向对象的 retain 方法,导致其引用计数加1 。

weak 则不会。

顾名思义:
strong属性指的是对这个对象强烈的占有!不管别人对它做过什么,反正你就是占有着!它对于你随叫随到。
weak指的是对这个对象弱弱的保持着联系,每次使用的时候你弱弱的问它一句“还在吗”,如果没人回应(变成nil),就说明它已经离开你了(大概是被系统残忍的回收了吧)。
只要有任何strong 指向某个对象A,ARC就不会摧毁它(A)。
而weak所指向的对象B,只要没有其他strong指向该对象(B),ARC会摧毁它(B)。

ViewController中的UIView Property要设置为WEAK还是STRONG

delegate为啥要用weak

NSString只有在来源是NSMutableString的时候必须要用copy,其他时候用strong修饰可以减小性能开销:here

assign, copy

assign:默认类型,setter方法直接赋值,不进行任何retain操作,不改变引用计数。一般用来处理基本数据类型。

copy:与retain处理流程一样,先对旧值release,再copy出新的对象,retainCount为1.为了减少对上下文的依赖而引入的机制。我理解为内容的拷贝,向内存申请一块空间,把原来的对象内容赋给它,令其引用计数为1。对copy属性要特别注意:被定义有copy属性的对象必须要 符合NSCopying协议,必须实现- (id)copyWithZone:(NSZone *)zone方法。

使用assign: 对基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char, 等等); 使用copy: 对NSString

属性的定义

推荐

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

属性的参数应该按照下面的顺序排列:原子性读写内存管理。这样做你的属性更容易修改正确,并且更好阅读。

可变对象

任何可以用一个可变的对象设置的((比如 NSString,NSArray,NSURLRequest))属性的内存管理类型必须是 copy 的。

这是为了确保防止在不明确的情况下修改被封装好的对象的值(译者注:比如执行 array(定义为 copy 的 NSArray 实例) = mutableArray,copy 属性会让 array 的 setter 方法为 array = [mutableArray copy], [mutableArray copy] 返回的是不可变的 NSArray 实例,就保证了正确性。用其他属性修饰符修饰,容易在直接赋值的时候,array 指向的是 NSMuatbleArray 的实例,在之后可以随意改变它的值,就容易出错)。

你应该同时避免暴露在公开的接口中可变的对象,因为这允许你的类的使用者改变类自己的内部表示并且破坏类的封装。你可以提供可以只读的属性来返回你对象的不可变的副本。

推荐

1
2
3
4
5
6
7
/* .h */
@property (nonatomic, readonly) NSArray *elements

/* .m */
- (NSArray *)elements {
return [self.mutableElements copy];
}

Lazy Init 懒加载

当实例化一个对象需要耗费很多资源,或者配置一次就要调用很多配置相关的方法而你又不想弄乱这些方法时,我们需要重写 getter 方法以延迟实例化,而不是在 init 方法里给对象分配内存。通常这种操作使用下面这样的模板:

1
2
3
4
5
6
7
8
9
- (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;
}

Categories

建议在category方法前加上小写前缀以及下划线。

推荐

1
2
3
@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end

不推荐

1
2
3
@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。

但是category则完全不一样,它是在运行期决议的。
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

其它

self的循环引用

当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 来引用对象,避免引用循环。

推荐

1
2
3
4
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];

不推荐

1
2
3
[self executeBlock:^(NSData *data, NSError *error) {
[self doSomethingWithData:data];
}];
文章目录
  1. 1. if
    1. 1.0.1. 推荐
    2. 1.0.2. 不推荐
  2. 1.1. 尤达表达式
    1. 1.1.1. 推荐
    2. 1.1.2. 不推荐
  3. 1.2. nil和BOOL检查
    1. 1.2.1. 推荐
    2. 1.2.2. 不推荐
  4. 1.3. 黄金大道
    1. 1.3.1. 推荐
    2. 1.3.2. 不推荐
  • 2. Switch
  • 3. 命名
    1. 3.1. 通用
    2. 3.2. 常量
      1. 3.2.1. 推荐
      2. 3.2.2. 不推荐
    3. 3.3. 方法
      1. 3.3.1. 推荐
      2. 3.3.2. 不推荐
  • 4.
    1. 4.1. 类名
    2. 4.2. Initializer 和 dealloc
    3. 4.3. Designated 和 Secondary 初始化方法
    4. 4.4. instancetype
    5. 4.5. 初始化模式
      1. 4.5.1. 类簇
      2. 4.5.2. 单例
  • 5. 属性
    1. 5.0.1. 推荐
    2. 5.0.2. 不推荐
    3. 5.0.3. Init 和 Dealloc
  • 5.1. ARC 中关于属性的修饰符
    1. 5.1.1. atomic, nonatomic
    2. 5.1.2. readwrite, readonly
    3. 5.1.3. strong, weak
    4. 5.1.4. assign, copy
  • 5.2. 属性的定义
    1. 5.2.1. 推荐
  • 5.3. 可变对象
    1. 5.3.1. 推荐
  • 5.4. Lazy Init 懒加载
  • 6. Categories
    1. 6.0.1. 推荐
    2. 6.0.2. 不推荐
  • 7. 其它
    1. 7.1. self的循环引用
      1. 7.1.1. 推荐
      2. 7.1.2. 不推荐
  • ,