Objective-C语言(一)熟悉Objective-C

本系列是根据《Effective Objective-C 2.0》一书中的系列文章,选开发中实践的经验之谈,汇集于此,便于查阅,或者为来访者提供一份参考。编排按《Effective Objective-C 2.0》中条目。

一、最佳实践

  • 在类的头文件中尽量少引用其他头文件;
  • 多用字面量语法,少用与之等价的方法;
  • 多用类型常量,少用#define预处理指令;
  • 用枚举表示状态、选项、状态码;

二、实践详解

2.1 头文件

在类的头文件中尽量少引用其他头文件

@class

“向前声明”或“前向引用”,仅仅是声明一个类名,并不会包含类的完整声明。
@class还能解决循环包含的问题。在实现这个接口的实现类中,如果需要引用这个类的实体变量或者方法之类的,还是需要#import@class中声明的类进来。

那么为什么还需要@class呢?因为作声明某个类来用,编译器并不会将类的实例变量或方法引入,其可以加快编译,减少编译时间。

另外,在实际中,会遇到——当解析某个文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引入了第一个头文件——循环包含,这时候,使用#import而非#include指令虽然不会导致死循环,但却意味着两个类里有一个无法被正确编译。这时候,采用@class仅作声明。

  • #import#include都能完整地包含某个文件的内容,#import能防止同一个文件被包含多次;
  • #import <> 用来包含系统自带的文件,#import “”用来包含自定义的文件
  • 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。以此来尽量降低类之间的耦合。
  • 要声明某个类遵循一项协议,精良移至“class continuation”分类中实现。如果不行,就把该协议单独放入一个头文件中,然后将其引入。

2.2 字面量

多用字面量语法,少用与之等价的方法。

字面量语法,其实是Objective-C 2.0添加的“语法糖”,方便程序员书写,提高可读性以及编译时检查等特性。

其中,涉及到类NSStringNSNumberNSArrayNSDictionary

字面量字符串

NSString *siteTitle = @"Mobiletuts+";
//假如不用字面量语法,那么上面可能会写成
NSString *siteTitle = [NSString stringWithUTF8String:"Mobiletuts"];

字面量数值

NSNumber *boolYES = [NSNumber numberWithBool:YES];
NSNumber *boolNO  = [NSNumber numberWithBool:NO];

NSNumber *charX = [NSNumber numberWithChar:'X'];

NSNumber *fortySevenInt = [NSNumber numberWithInt:47];
NSNumber *fortySevenUnsigned = [NSNumber numberWithUnsignedInt:47U];
NSNumber *fortySevenLong = [NSNumber numberWithLong:47L];

NSNumber *goldenRatioFloat = [NSNumber numberWithFloat:1.61803F];
NSNumber *goldenRatioDouble = [NSNumber numberWithDouble:1.61803];

采用字面量语法,上面可写为:

NSNumber *boolYES = @YES;
NSNumber *boolNO  = @NO;

NSNumber *charX = @'X';

NSNumber *fortySevenInt = @47;
NSNumber *fortySevenUnsigned = @47U;
NSNumber *fortySevenLong = @47L;

NSNumber *goldenRatioFloat = @1.61803F;
NSNumber *goldenRatioDouble = @1.61803;

字面量也适用于下述表达式:

int x = 5
float y = 6.32f
NSNumber *expressionNumber = @{x * y}

字面量数组

NSArray *instruments = [NSArray arrayWithObjects: @"Ocarina", @"Flute", @"Harp", nil];

使用字面量语法来创建:

NSArray *instruments = @[ @"Ocarina", @"Flute", @"Harp" ];

假如,要声明一个NSMutableArray数组,可以采用下面:

​ NSMutableArray *instrumentsMutable = [ @[ @”Ocarina”, @”Flute”, @”Harp” ] mutableCopy];

最后,需要注意的是,在用字面量语法创建数组时,若数组中有元素有nil,则会抛出异常,因为字面量语法实际上只是一种语法糖,其效果等同于是先创建了一个数组,然后把方括号内的多有对象都加到这个数组中。抛出的异常如下:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'*** - [__NSPlaceholderArray initWithObjects:count:]:attempt to insert nil object form objects[0]'

在改用字面量语法来创建数组时就会遇到这个问题,下面这段代码分别以两种语法创建数组:

id object1 = /*...*/
id object2 = /*...*/
id object3 = /*...*/

NSArray *arrayA = [NSArray arrayWithObjects:object1,object2,object3,nil];
NSArray *arrayB = @[object1,object2,object3];

假如,object1object3都指向了有效对象,而object2nil,会出现什么情况?

按字面量创建的arrayB会抛出异常,arrayA虽然能创建出来,但是其中只有object1一个对象。原因在于,“arrayWithObjects”方法会一次处理各个参数,直到发现nilobject2nil,所以方法提前结束。

这个微秒的差别表明,使用字面量语法更为安全。抛出异常令应用程序终止执行,这比创建好数组之后才发现元素个数少了要好。

字面量字典

创建一个字典:

NSArray *keys   = [NSArray arrayWithObjects:@"Character", @"Weapon", @"Hitpoints", nil];
NSArray *objects = [NSArray arrayWithObjects:@"Zelda", @"Sword", [NSNumber numberWithInt:50], nil];
NSDictionary *stats = [NSDictionary dictionaryWithObjects:objects forKeys:keys];

或者,采用更简洁的写法:

NSDictionary *stats = [NSDictionary dictionaryWithObjectsAndKeys:
                       @"Zelda", @"Character",
                       @"Sword", @"Weapon",
                       [NSNumber numberWithInt:50], @"Hitpoints",
                       nil];

而采用字面量语法:

NSDictionary *stats = @{ @"Character" : @"Zelda",
                     @"Weapon" : @"Sword",
                     @"Hitpoints" : @50 };

同样,在用字面量语法中,如果值是nil,与字面量创建数组表现相似,会抛出异常。

下标操作

“取下标”操作一般会用objectAtIndex方法:

NSString * flute = [instruments objectAtIndex:1];
NSString * flute = instruments[1];

假如,是可变数组,采用字面量写法:

instrumentsMutable[0] = @"Ocarina of Time";
//其对应的方法为:replaceObjectAtIndex:withObject

字典的读写也类似。

NSString *name = stats[@"Character"]; // Returns 'Zelda'
statsMutable[@"Weapon"] = @"Hammer";

//分别对应方法:
//objectForKey:
//setObject:forkey:

2.3 预处理

多用类型常量,少用#define预处理指令。

编写代码中常将常量写为:

#define ANIMATION_DURATION 0.3

预处理过程会把所有ANIMATION_DURATION一律替换成0.3,假设该指令声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。

更好的方式是:

static const NSTimerInterval kAnimationDuration = 0.3

首先,添加了类型信息,清楚地描述了常量的含义。

其次,要注意常量的名称,常用的命名法是:若常量局限于某“编译单元(一般只实现文件,即.m)”之内,则在前面加k;若常量在类之外可见,则通常以“类名”为前缀。

最后,要注意常量的位置。我们总喜欢在头文件里声明预处理指令,这是相当糟糕的,当常量名称有可能互相冲突更是如此。由于OC没有“命名空间”这一概念,所以避免将常量声明放在头文件里。即使采用static const这种方式也是如此。若不打算公开某个常量,则应该将其定义在使用该常量的实现文件里。

那么,为什么要用staticconst来修饰常量?

static表明的是作用域,意味着该变量尽在定义此变量的编译单元中可见,编译器每收到一个编译单元,就会输出一份“目标文件”。在Objective-C语境下,“编译单元”一次通常指每个类的实现文件。假如声明此变量时,不加static,那么编译器会为它创建一个“外部符号(external symbol)”。此时,若其他编译单元也声明了同名变量,就会抛出一条错误消息:

duplicate symbol _kAnimationDuration in:
    EOCAnimatedView.o
    EOCOtherView.o

const则声明为不可修改。

实际上,如果一个变量既声明为static,又声明为const,那么编译器根本不会创建符号,而是会像#define预处理指令一样,把所有遇到的变量头替换为常值,但是,这种方式具有类型信息。

那么,假如要对外公开一个常量要怎么办?

对外公开常量

有时候,需要对外公开常量。常见的情景就是在类代码中调用NSNotificationCenter以通知他人。那么通知名一般声明一个外界可见的常值变量。

此类变量需要放在“全局符号表(global symbol table)”中,以便可以在定义该常量的编译单元之外使用。其定义方式:

//in the header file 
extern NSString *const EOCStringConstant;

//in the implementation file
NSSting *const EOCStringConstant = @"VALUE"

编译器发现头文件中含有extern,就知道,在全局符号表中将会有一个EOCStringConstant的符号。即编译器无须查看其定义,就允许代码中使用此常量。因为它知道,当链接二进制文件后,肯定能找到这个常量。

此类常量必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件里。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。

  • 不要用预处理指令来定义常量,它不包含类型信息;
  • 在实现文件中使用static const来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无须为其名称加前缀;
  • 在头文件中使用extern来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。

2.4 枚举

用枚举表示状态、选项、状态码

enum {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle

};

typedef enum {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle

} UITableViewCellStyle;

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle
};

NS_OPTIONS

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

在iOS开发中,凡是需要以按位或操作来组合的枚举都应使用NS_OPTIONS定义。

若是枚举不需要互相组合,则应使用NS_ENUM来定义。

  • 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字;
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么久将各选项值定义为2的幂,以便通过按位或操作将其组合起来;
  • NS_ENUMNS_OPTIONS宏定义了来定义枚举类型,并指明底层数据类型。以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型;
  • 在处理枚举类型的switch语句中不要实现default分支。这样的话,假如新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。

参考

链接

  1. Objective-C Literals Clang 3.9 documentation

  2. Objective-C Literals

书籍

《Effective Objective-C 2.0》