《禅与Objective-C 编程艺术》笔记

前言

《禅与Objective-C 编程艺术》虽然内容不是很多,但是却又很多好用的Tips。可以将其作为OC的编码规范来使用。虽然OC逐渐在被Swift所替代,但是很多项目短时间是不会全部转向Swift的,而且本文有很多内容也是独立于编程语言的。总得来说是一本很不错的书,除了这本书外还有一本书叫《Effective Objective-C 2.0: 52 Specific Ways to Improve Your iOS and OS X Programs》,这两本书内容都差不多,后者比前者讲的更详细一些,对于初级程序猿来说会受益匪浅。

条件语句

语句体总是使用大括号来包围避免错误。

推荐

if (!error) {
   return success;
}

不推荐

if (!error)
return success;
或者
if (!error) return success;

不要使用尤达表达式(尤达表达式:使用常量去和变量比较)

推荐

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

不推荐

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

nil和BOOL检查使用感叹号来判断

因为nil是解释到NO所以没必要在条件语句里面把它和其他值比较。同时,不要直接把它和YES比较,因为YES的定义是1BOOL是8位的,实际上是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的

推荐

- (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 = object ? : [self createObject]; // 第二个参数返回和条件语句中已经检查的对象一致时

不推荐

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

result = object ? object : [self createObject]; // 第二个参数返回和条件语句中已经检查的对象一致时

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

推荐

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

Case语句

括号在Case语句中是非必要的,但当一个Case包含多行语句时,要加上括号

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

当switch语句里使用可枚举的变量的时候,default是不必要的,没有default有利于错误的排查

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

关于遍历

多用快速遍历(for in)或者block的方式遍历,少用常规for循环

Enumerated Types枚举类型推荐使用NS_ENUM()

用枚举表示状态,选项,状态码,可读性和可用性会大大增加

命名规则

尽量遵守Apple命名约定,推荐使用长的,描述性的方法和变量名

推荐

UIButton *settingsButton;

不推荐

UIButton *setBut; // 方法和变量尽量不要带set get 前缀

Constants常量

多用类型常量,少用#define预处理指令,有利于错误检测

常量命名:

常量如果局限于某个实现文件(.m)中时,使用前缀k。如果常量在类之外可见,则使用类名作为前缀。常量应该用static声明,并且不要使用#define,除非他就是明确作为一个宏。

推荐

static const NSTimeInterval kFadeOutAnimationDuration = 0.4; // 只在.m 可见

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4; // 类外可见

static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";

不推荐

static const NSTimeInterval fadeOutTime = 0.4;

 #define CompanyName @"Apple Inc.

类型常量如果在类外可见,则应该在.h文件中这样声明extern NSString *const ZOCCacheControllerDidClearCacheNotification;,在.m中这样声明NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";

方法命名在方法类型(-/+符号)后应该有一个空格,方法段之间也该有一个空格,参数名称之前总应有一个描述性关键词,尽量不使用and等连接词来表明多个参数

推荐

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

字面量/语法糖,多使用字面量来创建不可变的实例对象。

推荐

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];

美化代码

	if (user.isHappy) {
	    //Do something
	}
	else {
	    //Do something else
	}
	

不推荐:

	if (user.isHappy)
	{
	  //Do something
	} else {
	  //Do something else
	}
	[UIView animateWithDuration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(BOOL finished) {
                     // something
                 }];
	self.productsRequest = [[SKProductsRequest alloc]
   	  initWithProductIdentifiers:productIdentifiers];

代码组织

利用代码块

代码块如果在闭合的圆括号内的话,会返回最后语句的值。

NSURL *url = ({
    NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
    [NSURL URLWithString:urlString];
});

Pragma

使用 #Pragma Mark进行代码分组:

- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }

#pragma mark - View Lifecycle (View 的生命周期)

- (void)viewDidLoad { /* ... */ }
- (void)viewWillAppear:(BOOL)animated { /* ... */ }
- (void)didReceiveMemoryWarning { /* ... */ }

#pragma mark - Custom Accessors (自定义访问器)

- (void)setCustomProperty:(id)value { /* ... */ }
- (id)customProperty { /* ... */ }

#pragma mark - IBActions  

- (IBAction)submitData:(id)sender { /* ... */ }

#pragma mark - Public

- (void)publicMethod { /* ... */ }

#pragma mark - Private

- (void)zoc_privateMethod { /* ... */ }

#pragma mark - UITableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { /* ... */ }

#pragma mark - ZOCSuperclass

// ... 重载来自 ZOCSuperclass 的方法

#pragma mark - NSObject

- (NSString *)description { /* ... */ }

或者

image

忽略警告:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[myObj performSelector:mySelector withObject:name];

#pragma clang diagnostic pop
- (NSInteger)giveMeFive
{
    NSString *foo;
    #pragma unused (foo)

    return 5;
}

明确警告与错误标识

注释

接口与API设计

类的头文件(.h)中尽量少引入其他头文件,避免互相引用,降低耦合,减少编译时间

.h中要引入类时,只需要声明有这个类,具体细节不用暴露,使用向前声明(forward declaring)该类,即:@class XXXX。 在.m实现文件中使用该类时需要知道所有接口袭击,即需要import,#import XXXX

类名,应加上三个大写字母作为前缀(两个字母为Apple类保留,WTF😢),为了减少没有命名空间导致的问题

Initializer和dealloc初始化,将dealloc方法写在实现文件最前面,init方法放在dealloc之后。

使用ARC的话,dealloc方法中不用调用super dealloc,dealloc中只应释放引用,取消KVO的订阅或Notification,不做其他事情。

Designated和secondary Initializer

designated初始化方法提供所有的参数,一个类应该有且只有一个,secondary初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用Designated初始化方法的初始化方法。

eg:

@implementation ZOCEvent
 
 // designated初始化方法,一个类应该有且只有一个
- (instancetype)initWithTitle:(NSString *)title
                         date:(NSDate *)date
                     location:(CLLocation *)location
{
    self = [super init];
    if (self) {
        _title    = title;
        _date     = date;
        _location = location;
    }
    return self;
}
 
 // 一下为secondary初始化方法
- (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

在类继承中调用任何 designated 初始化方法都是合法的,而且应该保证 所有的 designated initializer 在类继承中是是从祖先(通常是 NSObject)到你的类向下调用的。 实际上这意味着第一个执行的初始化代码是最远的祖先,然后从顶向下的类继承,所有类都有机会执行他们特定的初始化代码。这样,你在你做你的特定的初始化工作前,所有你从超类继承的东西是不可用的状态。即使它的状态不明确,所有 Apple 的框架的 Framework 是保证遵守这个约定的,而且你的类也应该这样做。

定义一个新类的时候有三个不同方式:

第一个方案是最简单的:你不需要增加类的任何初始化逻辑,只需要依照父类的designated initializer。 当你希望提供额外的初始化逻辑的时候你可以重载 designated initializer。你只需要重载你的直接的超类的 designated initializer 并且确认你的实现调用了超类的方法。

在你希望提供你自己的初始化函数的时候,你应该遵守这三个步骤来保证正确的性:

通过宏来指定,正确实现例子:

@interface ZOCNewsViewController : UIViewController
 
- (instancetype)initWithNews:(ZOCNews *)news ZOC_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
- (instancetype)init ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
 
@end

@implementation ZOCNewsViewController
 
- (instancetype)initWithNews:(ZOCNews *)news
{
    // call to the immediate superclass's designated initializer
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _news = news;
    }
    return self;
}
 
// Override the immediate superclass's designated initializer (重载直接父类的  designated initializer)
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call the new designated initializer
    return [self initWithNews:nil];
}
 
@end

注意:你应该永远不从 designated initializer 里面调用一个 secondary initializer (如果secondary initializer 遵守约定,它会调用 designated initializer)。如果这样,调用很可能会调用一个子类重写的 init 方法并且陷入无限递归之中。

secondary initializer 是一种方便提供默认值、行为到 designated initializer 的 方法。也就是说,你不应该强制很多初始化操作在这样的方法里面,并且你应该一直假设这个方法不会得到调用。我们保证的是唯一被调用的方法是 designated initializer。 这意味着你的 designated initializer 总是应该调用其他的 secondary initializer 或者你 self 的 designated initializer。有时候,因为错误,可能打成了 super,这样会导致不符合上面提及的初始化顺序(在这个特别的例子里面,是跳过当前类的初始化)

一个相关的返回类型可以明确的规定用instancetype关键字作为返回类型

初始化模式

类簇(class cluster):一个在共有的抽象超类下设置一组私有子类的架构->抽象工厂设计模式,比如NSNumber,NSArray

特点:这个模式的精妙的地方在于,调用者可以完全不管子类,事实上,这可以用在设计一个库,可以用来交换实际的返回的类,而不用去管相关的细节,因为它们都遵从抽象超类的方法。

class cluster 的想法很简单,你经常有一个抽象类在初始化期间处理信息,经常作为一个构造器里面的参数或者环境中读取,来完成特定的逻辑并且实例化子类。这个”public facing” 应该知晓它的子类而且返回适合的私有子类。

单例:如果可能,请尽量避免使用单例而是依赖注入,使用单例请使用线程安全模式来创建共享实例dispatch_once()注意单例的滥用

单例模式应该运用于类及类的接口趋向于作为单例来使用的情况

属性,属性应该尽可能描述性的命名,避免缩写,而且是首字母小写的驼峰命名,应该总是使用settergetter方法访问属性,除了initdealloc方法。

eg:NSString *text,不推荐NSString* textNSString * text

属性getter/setter的好处:

getter特点:

注意:永远不能在init以及其他初始化函数里面和dealloc方法中用getter和setter方法,应该直接访问实例变量。 一个子类可以重载getter和setter并且尝试调用其他方法,访问属性他们可能没有完全初始化。

点符号:属性有关时尽量使用点符号,区分属性的访问还是方法调用

推荐

view.backgroundColor = [UIColor orangeColor];

[UIApplication sharedApplication].delegate;

不推荐

[view setBackgroundColor:[UIColor orangeColor]];

UIApplication.sharedApplication.delegate;

属性定义:属性参数按照原子性读写内存管理顺序排列,例如@property (nonatomic, readwrite, copy) NSString *name;

除非特别情况,原子性请使用nonatomic,iOS 中atomic带来的锁特别影响性能。

NSString,NSArray,NSDictionary等可变对象和block请使用copy关键字。block最早在栈中创建,通过Copy拷贝到堆里。可变对象参考深拷贝与浅拷贝

对于getter公有,setter私有的公开属性,需要在.h设置为readonly,并在类扩展中重新定义通用的属性为readwrite

@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object
@end
  
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object
@end

私有属性:应该在实现文件的类扩展(class extensions,没有名字的categories)中声明。

方法

参数断言:最好使用NSParameterAssert()来断言条件是否成立或是抛出一个异常。

私有方法:永远不要在私有方法前加上_前缀,此前缀被Apple保留。

相等性:当我们要自己实现相等性的时候,需要实现isEqualhash方法。

如果两个对象是被isEqual认为相等的,它们的 hash 方法需要返回一样的值。但是如果 hash 返回一样的值,并不能确保他们相等。自行百度。

分类(Categories),在方法名加上前缀和下划线

当一个类中逻辑方法很多时,可以将代码按照逻辑划分到不同的分类当中。将私有方法放到private分类中是一个好的实践。如果不想暴露实现细节则在class_continuation分类(没有名字的分类)中声明。

注意:

协议(Protocol),实现抽象接口,利于代码复用

在 Objective-C 的世界里面经常错过的一个东西是抽象接口。接口(interface)这个词通常指一个类的 .h 文件,但是它在 Java 程序员眼里有另外的含义: 一系列不依赖具体实现的方法的定义。

当实现一个 protocol 你总应该坚持里氏替换原则。 这个原则是:你应该可以取代任意接口(也就是Objective-C里的”protocol”)实现,而不用改变客户端或者相关实现。

可以参考代码

通过协议提供匿名对象

通知(Notification)

当你定义你自己的 NSNotification 的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。 你应该在公开的接口文件中将其声明为 extern 的, 并且在对应的实现文件里面定义。

// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Foo.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

对象的通讯

Block

当block作为异步接口的时候,尽量使用一个单独的block作为接口的最后一个参数。把需要提供的数据和错误信息整合到一个单独的block中,比分别提供要好

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion {
    if (objects) {
        // do something with the data
    }
    else {
        // some error occurred, 'error' variable should not be nil by contract
    }
}

注意:Apple 提供的一些同步接口在成功状态下向 error 参数(如果非 NULL) 写入了垃圾值,所以检查 error 的值可能出现问题。

关于block的一点知识

如果 block 没有在其他地方被保持,那么它会随着栈生存并且当栈帧(stack frame)返回的时候消失。 仅存在于栈上时,block对对象访问的内存管理和生命周期没有任何影响。

如果 block 需要在栈帧返回的时候存在,它们需要明确地被复制到堆上,这样,block 会像其他 Cocoa 对象一样增加引用计数。 当它们被复制的时候,它会带着它们的捕获作用域一起,retain 他们所有引用的对象。

如果一个 block引用了一个栈变量或指针,那么这个block初始化的时候会拥有这个变量或指针的const副本, 所以(被捕获之后再在栈中改变这个变量或指针的值)是不起作用的。

当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆里,复制完成之后, 无论是栈上的block还是刚刚产生在堆上的block(栈上block的副本)都会引用该变量在堆上的副本。

 ...
 CGFloat blockInt = 10;
 void (^playblock)(void) = ^{
     NSLog(@"blockInt = %zd", blockInt);
 };
 blockInt ++;
 playblock();
 ...

 //结果为:blockInt = 10

self的循环引用

当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 来引用对象,避免引用循环。 此外,把持有 block 的属性设置为 nil (比如 self.completionBlock = nil) 是一个好的实践。它会打破 block 捕获的作用域带来的引用循环。

// 多语句是需要在block里强引用一次
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingWithData:data];
        [strongSelf doSomethingWithData:data];
    }
}];

委托和数据源

委托模式 是 Apple 的框架里面使用广泛的模式,同时它是四人帮的书“设计模式”中的重要模式之一。 委托代理模式是单向的,消息的发送方(委托方)需要知道接收方(代理方)是谁,反过来就没有必要了。 对象之间耦合较松,发送方仅需知道它的代理方是否遵守相关 protocol 即可。

本质上,委托代理模式仅需要代理方提供一些回调方法,即代理方需要实现一系列空返回值的方法。

然而Apple并没有遵守改原则,比如TableView的Delegate和DataSource

为了分离概念,我们应该这样做:

委托模式(delegate pattern):事件发生的时候,委托者需要通知代理者。 数据源模式(datasource pattern): 委托者需要从数据源对象拉取数据。

@class ZOCSignUpViewController;

@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end


@interface ZOCSignUpViewController : UIViewController

@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;

@end

代理方法必须以调用者(即委托者)作为第一个参数,就像上面的例子一样。否则代理者无法区分不同的委托者实例。

继承

有时候你可能需要重载代理方法。考虑有两个 UIViewController 子类的情况:UIViewControllerA 和 UIViewControllerB,有下面的类继承关系。

UIViewControllerA 遵从 UITableViewDelegate 并且实现了 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

你可能会想要在 UIViewControllerB 中提供一个不同的实现,这个实现可能是这样子的:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

但如果超类没有实现该方法时,此时调用[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]方法,将使用 NSObject 的实现。 根据消息机制,会出现crash。

这时可以换一种实现方式:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
   CGFloat retVal = 0;
   if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
       retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
   }
   return retVal + 10.0f;
}

多重委托

多重委托是一个非常基础的概念,但是,大多数开发者对此非常不熟悉而使用 NSNotifications。多重委托实现方式有多重,可参考LBDelegateMatrioska,可自行百度。

使用NSCache来构建缓存,而非NSDictionary

NSCache提供优雅的自动删减功能,而且线程安全,不会像Dictionary一样拷贝Key.NSCache可与NSPurgeableData搭配使用,实现数据的自动清除功能。

Aspect Oriented Programming (AOP,面向切面编程)

Aspect Oriented Programming (AOP,面向切面编程) 在 Objective-C 社区内没有那么有名,但是 AOP 在运行时可以有巨大威力。 但是因为没有事实上的标准,Apple 也没有开箱即用的提供,也显得不重要,开发者都不怎么考虑它。

通常 AOP 被用来实现横向切面。统计与日志就是一个完美的例子。

我们使用运行时的特性来增加切面:

可参考Aspects