禅与Objective-C编程艺术

1.条件语句


1.1 Yonda表达式

名字起源于星球大战中Yonda大师的讲话方式,总是用倒装的语序

image.png

Yonda表达式是指:拿一个常量去跟一个变量比较而不是拿变量去跟常量比较。它就像是在表达 “蓝色是不是天空的颜色” 或者 “高个是不是这个男人的属性” 而不是 “天空是不是蓝的” 或者 “这个男人是不是高个子的”

推荐:

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

不推荐:

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

1.2 nil 和 BOOL检查

检查对象是否为nil推荐使用:

if (nil == myValue) {...

而不是:

if (myValue == nil) {...

如果不小心敲错成这样:

if (myValue = nil) {...

这是合法的语句,很难调试出错误,如果把 nil 放在左边,因为它不能被赋值,所以就不会发生这样的错误。

为了避免这些奇怪的问题,可以使用 !作为运算符,因为 nil 是解释到 NO。


NSString *testString = nil;

if (testString) {

    NSLog(@"the testString is not nil");

}

if (!testString) {

    NSLog(@"the testString is nil");

}

OUTPUT:the testString is nil

同时不要直接把它和 YES 比较,原因参考链接:https://www.jianshu.com/p/bb654c76a50d

1.3 黄金大道

将代码的重要部分放在黄金大道上,不要放在if分支里

推荐:

- (void)someMethod {

    if (![someOther boolValue]) {

        return;

    }

    // Do something important

}

不推荐:

- (void)someMethod {

    if ([someOther boolValue]) {

        // Do something important

    }

}

1.4 复杂的表达式

当你有一个复杂的if (condition) 语句的时候,你应该把condition提取出来赋给一个BOOL变量,让每个if语句的意义和逻辑体现出来。

推荐:

BOOL namecontainSwift = [sessionName containsString: @"Swift"];

BOOL isCurrentYear = [sessionDateComponent year] == 2014;

BOOL isSWiftSession = namecontainSwift && isCurrentYear;

if (isSWiftSession) {

    // Do something here

}

把条件提取成一个变量,而不是将一个复杂的表达式放在if后面。

2.命名


2.1Constant常量

推荐使用常量来代替字符串字面值和数字,这样能够方便服用,而且可以快速修改而不需要查找和替换。

常量应该用 static 声明为静态常量,而不要用 #define,除非它明确的作为一个宏来使用

推荐:

static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";

static const CGFloat ZOCImageThumbnailHeight = 50.0f;

不推荐:

#define CompanyName @"Apple Inc."

#define magicNumber 42

常量应该在头文件中以这样的形式暴露给外部,并在实现文件中为它赋值

extern NSString *const ZOCCacheControllerDidClearCacheNotification;

2.2方法命名

方法名与方法类型 (-/+ 符号)之间应该以空格间隔,尽可能少用“and”这个词,它不应该用来阐明有多个参数。

推荐:

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不推荐:

- (instancetype)initWith:(int)width and:(int)height; // Never do this.

2.3字面值

使用字面值来创建不可变的NSString, NSDictionary, NSArray 和 NSNumber对象。注意不要将 nil 传进NSArray 和 NSDictionary里,因为这样会导致崩溃。

推荐:

NSArray *names = @[@“Amy”, @“Matt”, @"Chris”];

NSDictionary *PM = @{@“iphone”:@“Kate”, @“ipad”: @“Kamal”};

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

3.Case语句


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

switch (menuType) {

    case ZOCEnumNone:

        // ...

        break;

    case ZOCEnumValue1:

        // ...

        break;

    case ZOCEnumValue2:

        // ...

        break;

}

4.类


4.1类名

类名应该以三个大写字母作为前缀(双字母前缀为Apple的类预留)

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

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

4.2Initializer 和 dealloc 初始化

  • 将dealloc方法放在实现文件的最前面(直接在@synthesize以及@dynamic之后)

  • init应该在dealloc方法后面。如果有多个初始化方法,指定初始化方法(designated initializer)应该放在最前面,间接初始化方法(secondary initializer)跟在后面,这样更有逻辑性。

如今有了ARC,dealloc方法几乎不需要实现,不过把init和dealloc放在一起可以从视觉上强调它们是一对的。通常,在init方法中做的事情需要在dealloc方法中撤销。

init方法应该是这样的结构:

- (instancetype)init {

    self = [super init]; // call the designated initializer

    if (self) {

        // Custom initialization

    }

    return self;

}

为什么设置 self 为 [super init] 的返回值,以及中间发生了什么呢?这是一个十分有趣的话题。

我们退一步讲:我们常常写 [[NSObject alloc] init] 这样的代码,从而淡化了 alloc 和 init 的区别。Objective-C 的这个特性叫做 两步创建 。 这意味着申请分配内存和初始化被分离成两步,alloc 和 init。

  • alloc 负责创建对象,这个过程包括分配足够的内存来保存对象,写入 isa 指针,初始化引用计数,以及重置所有实例变量。

  • init 负责初始化对象,这意味着使对象处于可用状态。这通常意味着为对象的实例变量赋予合理有用的值。

alloc 方法将返回一个有效的未初始化的对象实例。对这个实例发送的消息会被转换成一次objc_msgSend()函数的调用,形参self的实参是alloc返回的指针;这样self在所有方法的作用域内都能够被访问。按照惯例,为了完成两步创建,实例第一个被调用的方法将是init方法。注意,NSObject 在实现init时,只是简单返回了self。

关于init的约定还有一个重要的组成部分,这个方法可以通过返回nil来告诉调用者,初始化失败了;这样我们就能理解为什么总是要调用self = [super init],如果你的父类说自己初始化失败了,[suepr init]就会返回nil,self = nil, 那么你必须假定你正处于一个不稳定的状态,因此在你的实现里不要继续你的初始化并且也返回nil。如果不这样做,你可能会在你的实现里操作一个不可用的对象,它的行为时不可预测的,最终可能会导致你的程序崩溃。

Designated 和 Secondary 初始化方法

designated初始化方法是提供所有的参数,secondary初始化方法可以存在一个或者多个,并且提供一个或者更多的默认参数来调用designated初始化方法。

#import 

#import 

@implementation ZOCEvent

*// designated 初始化方法*

- (instancetype)initWithTitle:(NSString *)title

                         date:(NSDate *)date

                     location:(CLLocation *)loaction{

    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 Initializer

一个类应该只有一个designated初始化方法,其他的初始化方法应该调用这个designated的初始化方法。

在类继承中调用任何designated初始化方法都是合法的,而且应该保证所有的 designated initiazlier 都是从祖先开始往下调用的,也就是第一个执行的是最远的祖先,然后从顶向下的类继承,保证你的祖先都有机会执行他们特定的初始化代码。

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

  1. 不需要重载任何初始化函数

  2. 重载 designated initiazlier

  3. 定义一个新的 designated initializer

第一个方案:不需要增加类的任何初始化逻辑,只需要依照父类的designated initiazlier

第二个方案:当你需要提供额外的初始化逻辑时,你可以重载 designated initializer,

一个典型的例子是你创造UIViewController子类的时候重载initWithNibName:bundle:方法。

@implementation ZOCViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil

{

    // call to the superclass designated initializer

    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];

    if (self) {

        // Custom initialization (自定义的初始化过程)

    }

    return self;

}

@end

第三个方案:在你希望提供你自己的初始化函数的时候,你应该遵守着三个步骤:

  1. 定义你的designated initializer,确保调用了直接超类的 designated initializer

  2. 重载直接超类的 designated initializer。调用你的新的designated initializer.

  3. 为新的designated initializer写文档

正确实现的例子:

@implementation ZOCNewsViewController

*//定义你的 designated initializer*

- (instancetype)initWithNews:(ZOCNews *)news

{

    *// (调用直接超类的 designated initializer)*

    self = [super initWithNibName:nil bundle:nil];

    if (self) {

        _news = news;

    }

    return self;

}

//(重载直接父类的  designated initializer)

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil

{

  *  //  调用你的新的designated initializer*

    return [self initWithNews:nil];

}

@end

Cocoa充满了约定,这些约定可以帮助编辑器变得更加聪明。无论编译器是否遭遇alloc或者init方法,它会知道,即使返回类型都是id,这些方法总是返回接受到的类类型的实例。根据clang的定义,id可以被编译器提升到instancetype。在alloc或者init中,强烈建议对所有返回类的实例的类方法和实例方法使用intancetype类型。

单例

如果可能,请尽量避免使用单例而是依赖注入。如果一定要使用,请使用一个线程安全的模式来创建共享的实例。对于GCG,用dispatch_once()函数就OK。

不推荐:

+ (instancetype)sharedInstance

{

    static id sharedInstance;

    @synchronized(self) {

        if (sharedInstance == nil) {

            sharedInstance = [[MyClass alloc] init];

        }

    }

    return sharedInstance;

}

推荐:

+ (instancetype)sharedInstance

{

   static id sharedInstance = nil;

   static dispatch_once_t onceToken = 0;

   dispatch_once(&onceToken, ^{

      sharedInstance = [[self alloc] init];

   });

   return sharedInstance;

}

dispatch_once() 的优点是,它更快,而且语法上更干净,因为disptach_once()的意思就是“把一些东西执行一次”.

4.3属性

点符号

当使用 setter getter 方法的时候尽量使用点符号。应该总是用点符号来访问以及设置属性。

推荐:

view.backgroundColor = [UIColor orangeColor];

[UIApplication sharedApplication].delegate;

不推荐:

[view setBackgroundColor:[UIColor orangeColor]];

UIApplication.sharedApplication.delegate;

使用点符号会让表达更加清晰并且帮助区分属性访问和方法调用

属性定义@property (nonatomic, readwrite, copy) NSString *name;

属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。 这样做你的属性更容易修改正确,并且更好阅读。(译者注:习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作)

4.4方法

永远不要在你的私有方法前加上 _ 前缀。这个前缀是 Apple 保留的。不要冒重载苹果的私有方法的险。

4.5相等性

当你要实现相等性的时候记住这个约定:你需要同时实现isEqual 和 hash方法。如果两个对象是被isEqual认为相等的,它们的 hash 方法需要返回一样的值。但是如果 hash 返回一样的值,并不能确保他们相等。

5.Categories


6.Protocols


区分delegate和data source

7.NSNotification


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

因为你在头文件中暴露了符合,所以你应该按照统一的命名空间前缀法则,用类名前缀作为这个通知名字的前缀。

同时,用一个 Did/Will 这样的动词以及用 "Notifications" 后缀来命名这个通知也是一个好的实践。

// Foo.h

extern NSString * const ZOCFooDidBecomeBarNotification

// Foo.m

NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

8.美化代码


8.1缩进使用4个空格,永远不要使用tab

推荐:

if (user.isHappy) {

    //Do something

}

else {

    //Do something else

}

不推荐:

if (user.isHappy)

{

  //Do something

} else {

  //Do something else

}

8.2应该总是让冒号对齐

有一些方法签名可能超过三个冒号,用冒号对齐可以让代码更具有可读性。即使有代码块存在,也应该用冒号对齐方法。

推荐:

[UIView animateWithDuration:1.0

                 animations:^{

                     // something

                 }

                 completion:^(BOOL finished) {

                     // something

                 }];

不推荐:

[UIView animateWithDuration:1.0 animations:^{

    // something

} completion:^(BOOL finished) {

    // something

}];

9.代码组织


9.1利用代码块

一个 GCC 非常模糊的特性,以及 Clang 也有的特性是,代码块如果在闭合的圆括号内的话,会返回最后语句的值

不推荐:

NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];

NSURL *url = [NSURL URLWithString:urlString];

推荐:

NSURL *url = ({

    NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];

    [NSURL URLWithString:urlString];

});

这样做的好处:

  1. 特别适合组织小块的代码

  2. 给了读者一个重要的入口并且减少相关干扰

  3. 所有的变量都在代码块中,也就是只在代码块的区域有效,可以减少对其他作用域的命名污染

9.2Pragma

建议使用#pragma mark - 来分离:

  • 不同功能组的方法

  • protocols的实现

  • 对父类方法的重写

大多数 iOS 开发者平时并没有和很多编译器选项打交道。一些选项是对控制严格检查(或者不检查)你的代码或者错误的。有时候,你想要用 pragma 直接产生一个异常,临时打断编译器的行为。

当你使用ARC的时候,编译器帮你插入了内存管理相关的调用。但是这样可能产生一些烦人的事情。比如你使用 NSSelectorFromString 来动态地产生一个 selector 调用的时候,ARC不知道这个方法是哪个并且不知道应该用那种内存管理方法,你会被提示 performSelector may cause a leak because its selector is unknown(执行 selector 可能导致泄漏,因为这个 selector 是未知的).

如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告

#pragma clang diagnostic push

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

[myObj performSelector:mySelector withObject:name];

#pragma clang diagnostic pop

忽略没有被使用的变量

告诉你申明的变量它将不会被使用,这种做法很有用。大多数情况下,你希望移除这些引用来(稍微地)提高性能,但是有时候你希望保留它们。为什么?或许它们以后有用,或者有些特性只是暂时移除。无论如何,一个消除这些警告的好方法是用相关语句进行注解,使用 #pragma unused():

- (NSInteger)giveMeFive

{

    NSString *foo;

    #pragma unused (foo)

    return 5;

}

9.3自己为暂时无法解决的错误和有可能出现的错误添加提示

// error 提醒,红色

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor

{

    #error Whoa, buddy, you need to check for zero here!

    return (dividend / divisor);

}
// warning 提醒,黄色

- (float)divide:(float)dividend by:(float)divisor

{

    #warning Dude, don't compare floating point numbers like this!

    if (divisor != 0.0) {

        return (dividend / divisor);

    }

    else {

        return NAN;

    }

}

10.对象间的通信


10.1 Block

10.2 委托和数据源

你可能感兴趣的:(禅与Objective-C编程艺术)