前言
我们在 2013 年 11 月份开始写这本书,最初的目标是提供一份如何编写干净漂亮的
Objective-C 代码的指南:现在虽然有很多指南,但是它们都是有一些问题的。我们不想介绍一些死板的规定,我们想提供一个在开发者们之间写更一致的代码的途径。随时间的推移,这本书开始转向介绍如何设计和构建优秀的代码。
这本书的观点是代码不仅是可以编译的,同时应该是 “有效” 的。好的代码有一些特性:简明,自我解释,优秀的组织,良好的文档,良好的命名,优秀的设计以及可以被久经考验。
本书的一个理念是是代码的清晰性优先于性能,同时阐述为什么应该这么做。
虽然所有的代码都是 Objective-C 写的,但是一些主题是通用的,并且独立于编程语言。
Swift
在 2014 年 6 月 6 日,苹果发布了面向 iOS 和 Mac 开发的新语言:
Swift。
这个新语言与 Objective-C 截然不同。所以,我们改变了写这本书的计划。我们决定发布这本书当前的状态,而不是继续书写我们原来计划写下去的主题。
Objective-C 没有消失,但是现在用一个慢慢失去关注的语言来继续写这本书并不是一个明智的选择。
贡献给社区
我们将这本书免费发布并且贡献给社区,因为我们希望提供给读者一些有价值的内容。如果你能学到至少一条最佳实践,我们的目的就达到了。
我们已经非常用心地打磨了这些文字,但是仍然可能有一些拼写或者其他错误。我们非常希望读者给我们一个反馈或者建议,以来改善本书。所以如果有什么问题的话,请联系我们。我们非常欢迎各种
pull-request。
条件语句
为了避免错误,条件语句体应该总是被大括号包围,即使可以不这样做(比如,条件语句体只有一行内容)。可能的错误是:多加了第二行,并且误以为它是
if 语句体里面的。此外,更危险的是,如果把 if 语句体里的一行注释掉了,之后的一行代码会成为 if
语句里的代码。
推荐:
if (!error) { return success; } |
不推荐:
if (!error) return success;
|
或者
if (!error) return success;
|
在 2014年2月 苹果的 SSL/TLS 实现里面发现了知名的 goto fail 错误。
代码在这里:
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;
} |
显而易见,这里有没有括号包围的2行连续的 goto fail; 。我们当然不希望写出上面的代码导致错误。
此外,在其他条件语句里面也应该按照这种风格统一,这样更便于检查。
尤达表达式
不要使用尤达表达式。尤达表达式是指,拿一个常量去和变量比较而不是拿变量去和常量比较。它就像是在表达 “蓝色是不是天空的颜色”
或者 “高个是不是这个男人的属性” 而不是 “天空是不是蓝的” 或者 “这个男人是不是高个子的”
推荐:
if ([myValue isEqual:@42]) { ...
|
不推荐:
if ([@42 isEqual:myValue]) { ...
|
nil 和 BOOL 检查
类似于 Yoda 表达式,nil 检查的方式也是存在争议的。一些 notous
库像这样检查对象是否为 nil:
if (nil == myValue) { ...
|
或许有人会提出这是错的,因为在 nil 作为一个常量的情况下,这样做就像
Yoda 表达式了。 但是一些程序员这么做的原因是为了避免调试的困难,看下面的代码:
if (myValue == nil) { ...
|
如果程序员敲错成这样:
这是合法的语句,但是即使你是一个丰富经验的程序员,即使盯着眼睛瞧上好多遍也很难调试出错误。但是如果把
nil 放在左边,因为它不能被赋值,所以就不会发生这样的错误。 如果程序员这样做,他/她就可以轻松检查出可能的原因,比一遍遍检查敲下的代码要好很多。
为了避免这些奇怪的问题,可以用感叹号来作为运算符。因为 nil 是 解释到 NO,所以没必要在条件语句里面把它和其他值比较。同时,不要直接把它和
YES 比较,因为 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 语句是 OK 的。这样可以避免 Cyclomatic 复杂性 (译者注: https://en.wikipedia.org/wiki/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
} |
三元运算符
三元运算符 ? 应该只用在它能让代码更加清楚的地方。 一个条件语句的所有的变量应该是已经被求值了的。类似
if 语句,计算多个条件子句通常会让语句更加难以理解。或者可以把它们重构到实例变量里面。
推荐:
不推荐:
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 参数(如果它非 NULL)写入垃圾值(garbage
values),所以如果检查 error 的值可能导致错误 (甚至崩溃)。
Case语句
除非编译器强制要求,括号在 case 语句里面是不必要的。但是当一个 case 包含了多行语句的时候,需要加上括号。
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 继续执行。
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,如果新的值加入到 enum,程序员会马上收到一个 warning
通知
Enumeration value 'ZOCEnumValue3' not handled in switch.(枚举类型 'ZOCEnumValue3' 没有被 switch 处理) |
枚举类型
当使用 enum 的时候,建议使用新的固定的基础类型定义,因它有更强大的的类型检查和代码补全。 SDK
现在有一个 宏来鼓励和促进使用固定类型定义 - NS_ENUM()
例子:
typedef NS_ENUM(NSUInteger, ZOCMachineState) { ZOCMachineStateNone, ZOCMachineStateIdle, ZOCMachineStateRunning, ZOCMachineStatePaused }; |
命名
通用的约定
尽可能遵守 Apple 的命名约定,尤其是和 内存管理规则 (NARC) 相关的地方。
推荐使用长的、描述性的方法和变量名
推荐:
UIButton *settingsButton; |
不推荐:
常量
常量应该使用驼峰命名法,并且为了清楚,应该用相关的类名作为前缀。
推荐:
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 字面值应该用在任何创建不可变的实例对象。特别小心不要把
nil 放进 NSArray 和 NSDictionary 里,这会导致崩溃
例子:
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 的实例,但是在这种情形下你需要停下来思考下。另一个方面,一些新手看到后可能会对可变和不可变对象的分歧感到不舒服。他/她可能对创造一个可变对象的副本不是很熟悉(当然这并不是说这个知识不重要)。当然,这并不是说存在绝对的错误,只是可用性(包括可读性)有一些问题。
|