禅与Objective-C编程艺术-1

编程和「禅」有关系吗?

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

本章包含规范:条件语句、Case 语句、命名

条件语句(Conditionals)

虽然条件语句体可以不写在大括号里(比如就一行),但是为了避免错误还是应该把条件语句体写在大括号内部。比如不写在括号里时,插入的第二行代码会误以为是条件语句体的一部分。还有个更致命的危险就是,当仅有的一句 if 语句被注释掉时,无意之间下一行代码就成为 if 语句的一部分了。

推荐:

if (!error) {
return success;
}

不推荐:

if (!error)
return success;

if (!error) return success;

2014年二月 苹果的SSL/TLS的实现中就发现了众所周知的goto fail错误。

错误原因是在 if 条件后重复出现了 goto 语句,把 if 分支写在括号里就能避免这类问题。

代码如下

static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams, uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
...
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
...
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}

很明显,两行连续的 goto fail; 没有写在括号里。我们肯定不想出现上述类似的错误。

还有在其他条件语句中也应保持这种代码风格,便于检查。

尤达表达式

尽量别用尤达表达式。尤达表达式就是用常量去和变量比较,而不是用变量跟常量比较。比如正常表达是「天空是不是蓝色的」或者「这个人是不是高个子」,尤达表达式就会说成「蓝色是不是天空的颜色」或者「高个子是不是这个人的属性」。

Yoda

推荐:

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

不推荐:

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

nil 和 BOOL 检查

和尤达表达式类似,nil检查方式也是存在争议的。 一些 notous 库像这样检查对象是否为 nil:

if (nil == myValue) { ...

因为 nil 为常量的情况下,这种情况就很像尤达表达式,或许有人就会提出这种写法是错误的。其实很多程序员这么做的原因是为了避免调试困难,请看如下代码:

if (myValue == nil) { ...

如果不小心写成这样:

if (myValue = nil) { ...

这种表达是符合语法的,就算你是个经验丰富的程序员也很难能找出错误。但是如果把 nil 像变量一样放在左边这种错误就不会发生因为它不能被赋值。如果程序员用这种办法,就可以很容发现一些可能的原因,比反复检查写过的代码要好很多。

避免这些奇怪问题的办法,可以用感叹号来作为运算符。因为 nil 的意思就是 NO ,没必要在条件语句里把它跟其他值比较。还有千万不要直接去和 YSE 比较,因为 YES 的定义是 1 ,而 BOOL 是 8 bit 的,实际上是 char 类型。

推荐:

if (someObject) { ...
if (![someObject boolValue]) { ...
if (!someObject) { ...

不推荐:

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

这样也能提高文件的统一性和可读性。

黄金法则

当写条件语句的时候,代码左侧留有空隙是让人愉悦的黄金法则。就是说,不要嵌套 if 语句。多写几个 return 没啥问题。这会避免cyclomatic complexity的增加并且让代码更加易读,因为方法的重要部分都未嵌套在分支内,你就可以很清楚的找到相关代码。

推荐:

- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}

不推荐:

- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}

复杂条件语句

当在 if 分句中有一个复杂条件时,应该把它抽取出来赋值给一个 BOOL 变量,这样逻辑就会更清晰同时每个单独的条件语句的含义都很明确。

BOOL nameContainsSwift = [sessionName containsString:@"Swift"];
BOOL isCurrentYear = [sessionDateCompontents year] == 2014;
BOOL isSwiftSession = nameContainsSwift && isCurrentYear;
if (isSwiftSession) {
// Do something very cool
}

三目运算符

三目运算符 ? 只应该用在让代码更清晰简洁的地方。单独的条件语句通常都应该是计算好值了的。对于条件语句来说,多个条件子句的计算会让代码更加难懂,或者可以把他们重构到实例变量里面。

推荐:

result = a > b ? x : y;

不推荐:

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

当三目运算的第二个参数( if 分支)返回的对象和条件语句中检查已存在的对象相同的时候,如下语法更优雅:

推荐:

result = object ? : [self createObject];

不推荐:

result = object ? object : [self createObject];

错误处理

当方法返回一个错误参数的引用时,检查返回值,而非出错的变量

推荐:

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

此外,一些苹果的 API 在请求成功的情况下会对 error 参数(如果非空)写入垃圾值(garbage values),所以如果检查 error 的值可能会导致出错(甚至崩溃)。

Case 语句

除非编译器强制要求,否则在 case 语句中不是必须写括号的。

要是一个 case 有多行,那就需要加上括号。

switch (condition) {
case 1:
// ...
break;
case 2: {
// ...
// Multi-line example using braces
break;
}
case 3:
// ...
break;
default:
// ...
break;
}

有很多时候多个 case 中执行同一段代码,那就用上 fall-through了。fall-through 就是移除 casebreak 语句然后让下面的 case 继续执行。

switch (condition) {
case 1:
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}

当在 switch 中用枚举变量时,就不用非要写 default 了。例如:

switch (menuType) {
case ZOCEnumNone:
// ...
break;
case ZOCEnumValue1:
// ...
break;
case ZOCEnumValue2:
// ...
break;
}

此外,为了避免使用默认的 case ,如果枚举增添了新的值,程序员就会立即收到一个警告:

Enumeration value ‘ZOCEnumValue3’ not handled in switch.

枚举类型 ZOCEnumValue3 未被 switch 处理

枚举类型

当使用 enum 时,建议用固定基础类型标准,因为它具备更强的类型检查和和代码补全能力。SDK 现在提供了一个宏来促进和鼓励使用固定基础类型- NS_ENUM()

例子:

typedef NS_ENUM(NSUInteger, ZOCMachineState) {
ZOCMachineStateNone,
ZOCMachineStateIdle,
ZOCMachineStateRunning,
ZOCMachineStatePaused
};

命名

惯例

要尽可能的遵守苹果的命名规范,尤其是跟内存管理规则(NARC)相关的部分。

推荐使用详尽的、语意强的的方法名和变量名。

推荐:

UIButton *settingsButton;

不推荐:

UIButton *setBut;

常量

常量的应该遵循驼峰命名法,为了语意清晰,应该使用相关的类名作为前缀。

推荐:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;

不推荐:

static const NSTimeInterval fadeOutTime = 0.4;

字符串和数字应该尽量抽离常量,这样便于复用,而且在替换的时候很快捷,也不需要到处查找。常量应该用 static声明,除了特别明确的要用宏定义,否则就别用#define了。

推荐:

static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;

不推荐:

#define CompanyName @"Apple Inc."
#define magicNumber 42

interface文件中,暴露在外部的常量应该遵循如下写法:

extern NSString *const ZOCCacheControllerDidClearCacheNotification;

在实现文件中,实现对之前常量的定义。

对于公共常量需要加上一个命名空间前缀。尽管在实现文件中常量也会有一些其他写法,那么也没必要一定遵循这个规则。

方法

对于方法签名,在方法类型(-/+)后应该有一个空格。方法段落之间也应该有一个空格(符合 Apple 的规范)。在参数名之前应该有一个描述性关键字。

要谨慎使用 and 命名,它不应该用作来表示有多个参数,比如下面initWithWidth:height:的例子。

推荐:

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

字面量

创建任何不可变的实例对象时,应该用NSString, NSDictionary, NSArray, 和 NSNumber 这些对象。特别注意的是别在NSArrayNSDictionary中放入nil,这样会导致崩溃。

例如:

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这个类的实例,这种情况你就得停下来思考一下这是怎么回事。

二,一些新手以他们的阅历,就会对可变对象与不可变对象之间的矛盾感到疑惑。他/她可能对创建一个可变副本不是很熟悉(当然也不是说这部分知识不重要)。

当然,这都不是什么绝对性的错误,更多是在说可用性(包括可读性)方面的问题。