禅与 Objective-C 编程艺术

Posted iOS开发by唐巧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了禅与 Objective-C 编程艺术相关的知识,希望对你有一定的参考价值。

前言

这是一本开源的电子书,最近经过林翔宇、庞博两位译者翻译成了中文。我读完后,深感每一个ios开发都应该学习一下本文。文章采用了CC共享协议,所以我转载部分内容到这里,希望大家有时间都去把完整的原文读一遍。

以下是摘抄的部分内容

条件语句

为了避免错误,条件语句体应该总是被大括号包围,即使可以不这样做(比如,条件语句体只有一行内容)。可能的错误是:多加了第二行,并且误以为它是 if 语句体里面的。此外,更危险的是,如果把 if 语句体里的一行注释掉了,之后的一行代码会成为 if 语句里的代码。

推荐:

if (!error) {
    return success;
}

不推荐:

if (!error)
    return success;

或者

if (!error) return success;

在 2014年2月 苹果的 SSL/TLS 实现里面发现了知名的 错误。

代码在这里:

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) { ...

如果程序员敲错成这样:

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 复杂性,并且让代码更加容易阅读。因为你的方法的重要部分没有嵌套在分支上,你可以很清楚地找到相关的代码。

推荐:

- (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 : 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 参数(如果它非 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 的命名约定,尤其是和 () 相关的地方。

推荐使用长的、描述性的方法和变量名

推荐:

UIButton *settingsButton;

不推荐:

UIButton *setBut;

常量

常量应该使用驼峰命名法,并且为了清楚,应该用相关的类名作为前缀。

推荐:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;

不推荐:

static const NSTimeInterval fadeOutTime = 0.4;

常量应该尽量使用 in-line 的字符串字面值或者数字,这样便于经常用到的时候复用,并且可以快速修改而避免查找和替换。 常量应该用 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.

点击“阅读原文”,查看完整的原文。