0%

禅与 Objective-C 编程艺术读书笔记

禅与 Objective-C 编程艺术读书笔记

条件语句

条件语句体应该总是被大括号包围。尽管有时候你可以不使用大括号(比如,条件语句体只有一行内容),但是这样做会带来问题隐患。比如,增加一行代码时,你可能会误以为它是 if 语句体里面的。此外,更危险的是,如果把 if 后面的那行代码注释掉,之后的一行代码会成为 if 语句里的代码。

写条件语言的时候要用大括号

尤达表达式

不要使用尤达表达式。尤达表达式是指,拿一个常量去和变量比较而不是拿变量去和常量比较。

例子:

1
2
3
4
//推荐
if ([myValue isEqual:@42]){···}
//不推荐
if ([@42isEqual: myValue]){···}

nil和BOOL检查

有些框架中检查nil的时候会这样写:

if (nil == myValue){···}

这种写法是像尤达表达式,拿常量和变量去比较。

这样写的原因是为了方便排查错误。我们正常的写法是这样写:

if (myValue == nil){···}

但是有的程序员会丢掉一个=号,像这样:

if (myValue = nil){···}

这样就相当于把nil赋值给了myValue,这样的错误比较难调试。如果反过来写的话nil是不能被赋值的。这样比较好调试。

为了避免这类错误,不要用这种方法去判断是否为真。使用感叹号来进行判断。可以这样去判断:

1
if (!myValue){···}

黄金大道

在使用条件语句编程时,代码的左边距应该是一条“黄金”或者“快乐”的大道。 也就是说,不要嵌套 if 语句。使用多个 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
}
}

意思就是不要把重要的逻辑代码放在条件语句中,如果重要的逻辑代码中还有条件语句,那这样就会造成循环的复杂。这样的语法格式参考swift中的guard语法。参考我的这篇文章

复杂的表达式

当你有一个复杂的 if 子句的时候,你应该把它们提取出来赋给一个 BOOL 变量,这样可以让逻辑更清楚,而且让每个子句的意义体现出来。

1
2
3
4
5
6
7
BOOL nameContainsSwift  = [sessionName containsString:@"Swift"];
BOOL isCurrentYear = [sessionDateCompontents year] == 2014;
BOOL isSwiftSession = nameContainsSwift && isCurrentYear;

if (isSwiftSession) {
// Do something very cool
}

三元运算符

三元运算符 ? 应该只用在它能让代码更加清楚的地方。 一个条件语句的所有的变量应该是已经被求值了的。类似 if 语句,计算多个条件子句通常会让语句更加难以理解。或者可以把它们重构到实例变量里面。

推荐:

result = a > b ? x : y;

不推荐:

result = a > b ? x = c > d ? c : d : y;

推荐:

result = object ? : [self createObject];

不推荐:

result = object ? object : [self createObject];

使用三元运算符是为了让代码更加易读,而不是为了使用它而用它。

错误处理

有些方法通通过参数返回 error 的引用,使用这样的方法时应当检查方法的返回值,而非 error 的引用。

推荐:

1
2
3
4
NSError *error = nil;
if (![self trySomethingWithError:&error]) {
// Handle Error
}

Case语句

除非编译器强制要求,括号在case语句里面是不必要的。但是当一个case包括多行语句的时候,需要加上括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (condition) {
case 1:
// ...
break;
case 2: {
// ...
// Multi-line example using braces
break;
}
case 3:
// ...
break;
default:
// ...
break;
}

有时候可以使用 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;
}

当在 switch 语句里面使用一个可枚举的变量的时候,default 是不必要的。比如:

1
2
3
4
5
6
7
8
9
10
11
switch (menuType) {
case ZOCEnumNone:
// ...
break;
case ZOCEnumValue1:
// ...
break;
case ZOCEnumValue2:
// ...
break;
}

此外,为了避免使用默认的 case,如果新的值加入到 enum,程序员会马上收到一个 warning 通知

枚举类型

当使用 enum 的时候,建议使用新的固定的基础类型定义,因它有更强大的的类型检查和代码补全。 SDK 现在有一个 宏来鼓励和促进使用固定类型定义 - NS_ENUM()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//推荐
typedef NS_ENUM(NSUInteger, ZOCMachineState) {
ZOCMachineStateNone,
ZOCMachineStateIdle,
ZOCMachineStateRunning,
ZOCMachineStatePaused
};
//不推荐
typedef enum{
ZOCMachineStateNone,
ZOCMachineStateIdle,
ZOCMachineStateRunning,
ZOCMachineStatePaused
}ZOCMachineState;

命名

尽可能的遵守Apple的命名约定,尤其是和 内存管理规则 (NARC) 相关的地方。推荐使用长的、描述性的方法和变量名。

例如:

1
2
3
4
//推荐
UIButton *settingsButton;
//不推荐
UIButton *setBut;

Constants常量

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

例如:

1
2
3
4
//推荐
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
//不推荐
static const NSTimeInterval fadeOutTime = 0.4;

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

1
2
3
4
5
6
//推荐
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
//不推荐
#define CompanyName @"Apple Inc."
#define magicNumber 42

常量应该在头文件中以这样的形式暴露给外部:

1
extern NSString *const ZOCCacheControllerDidClearCacheNotification;

并在实现文件中为它赋值。

只有公有的常量才需要添加命名空间作为前缀。尽管实现文件中私有常量的命名可以遵循另外一种模式,你仍旧可以遵循这个规则。

方法

方法名与方法类型 (-/+ 符号)之间应该以空格间隔。方法段之间也应该以空格间隔(以符合 Apple 风格)。参数前应该总是有一个描述性的关键词。尽可能少用 “and” 这个词。它不应该用来阐明有多个参数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
//推荐
- (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;

//不推荐
- (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.

字面值

多用字面量语法,少用与之等价的方法。

例如:

1
2
3
4
5
6
7
8
9
10
//推荐
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;
//不推荐
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];

如果要用到这些类的可变副本,我们推荐使用 NSMutableArray, NSMutableString 这样的类。
应该避免下面这样:

NSMutableArray *aMutableArray = [@[] mutableCopy];

这种写法效率和可读性都有问题。

效率方面,一个不必要的不可变对象被创建后立马被废弃了;虽然这并不会让你的 App 变慢(除非这个方法被频繁调用),但是确实没必要为了少打几个字而这样做。

可读性方面,存在两个问题:第一个问题是当你浏览代码并看见 @[] 的时候,你首先联想到的是 NSArray 实例,但是在这种情形下你需要停下来深思熟虑的检查;

另一个问题是,一些新手以他的水平看到你的代码后可能会对这是一个可变对象还是一个不可变对象产生分歧。他/她可能不熟悉可变拷贝构造的含义(这并不是说这个知识不重要)。

当然,不存在绝对的错误,我们只是讨论代码的可用性(包括可读性)。

类名

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

一些开发者在定义模型对象时并不遵循这个规范(对于 Core Data 对象,我们更应该遵循这个规范)。我们建议在定义 Core Data 对象时严格遵循这个约定,因为最终你可能需要把你的 Managed Object Model(托管对象模型)与其他(第三方库)的 MOMs(Managed Object Model)合并。

你可能注意到了,这本书里类的前缀(不仅仅是类,也包括公开的常量、Protocol 等的前缀)是ZOC。

另一个好的类的命名规范:当你创建一个子类的时候,你应该把说明性的部分放在前缀和父类名的在中间。

举个例子:如果你有一个 ZOCNetworkClient 类,子类的名字会是ZOCTwitterNetworkClient (注意 “Twitter” 在 “ZOC” 和 “NetworkClient“ 之间); 按照这个约定, 一个UIViewController 的子类会是 ZOCTimelineViewController.

Initializer 和 dealloc 初始化