Effective Objective-C 2.0 读书笔记 -- 熟悉Objective-C语言

看到Effective这个词,大家一定会想到《Effective C++》、《Effective Java》等业界名著,那些书里汇聚了多项实用技巧,又系统而深入的讲解了各种编程知识。那么,《Effective Objective-C 2.0》也是如此。

作为Mac OS X与iOS应用程序的开发语言,Objective-C作为首选。那么,它有哪些需要注意的呢?

起源

Objective-C与C++、Java一样,是面向对象的语言,是由Smalltalk演化而来。Smalltalk是消息型语言的鼻祖。消息与函数调用之间的区别看上去就像这样:

1
2
3
4
5
6
7
//Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];

//Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);

关键区别在于:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。

Objective-C是C的“超集”(superset),所以C语言中的所有功能在编写Objective-C代码时依然适用。理解C语言的内存模型(memory model),有助于理解Objective-C的内存模型及其“引用计数”(reference counting)机制的工作原理。Objective-C语言中的指针是用来指示对象的。

关于使用头文件

主要使用 import 关键字。然而,我们在 .h 文件中一般首选使用 @class 关键字,它能“向前声明”一个类。对于不需要知道类细节的情况下我们使用它。否则不会轻易使用 import 来引入整个头文件。

过多的引入头文件,会增加编译时间。这就是我们多使用 @class 关键字的直接原因。

除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用“向前声明”来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。

有时无法使用“向前声明”,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

字面量语法

在编写Objective-C程序时,总会用到某几个类,它们属于Foundation框架。虽然从技术上来说,不用Foundation框架也能写出Objective-C代码,但是实际上却经常要用到此框架。这几个类是NSString、NUNumber、NSArray、NSDictionary。从类名上即可看出各自所表示的数据结构。

Objective-C以语法繁杂而著称。不过从Objective-C 1.0起,有一种简单的方式能创建NSString 对象。这就是“字符串字面量”(string literal),其语法如下:

1
NSString *string = @"Effective Objective-C 2.0";

字面数值

1
2
3
NSNumber *number = [NSNumber numberWithInt:10];
//等价于
NSNumber *number = @10;

更多表示:

1
2
3
4
5
NSNumber *intNumber = @11;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.1415926;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'ABC';

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

1
2
3
int x =5;
float y = 6.5f
NSNumber *expressionNumber = @(x * y);

字面量数组

1
2
3
NSarray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];
// 等价于
NSarray *animals = @[@"cat", @"dog", @"mouse", @"badger"];

使用数组

1
2
3
NSString *dog = [animals objectAtIndex:1];
// 等价于
NSString *dog = animals[1];

字面量字典

1
2
3
NSDictionary *personData = [NSDictionary dictionaryWithObjectsAnsKeys:@"Matt", @"firstName", @"Galloway", @"lastName", [NSNumber numberWithInt:28], @"age", nil];
// 等价于
NSDictionary *personData = @{@"firstName":@"Matt", @"lastName":@"Galloway", @"age":[NSNumber numberWithInt:28]};

使用字典

1
2
3
NSString *lastName = [personData objectForKey:@"lastName"];
// 等价于
NSString *lastName = personData[@"lastName"];

可变数组和字典

1
2
3
4
5
[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"Galloway" forKey:@"lastName"];
// 等价于
mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";

局限性

字面量语法有个小小的限制,就是除了字符串以外,所创建出来的对象必须属于Foundation框架才行。如果自定义了这些类的子类,则无法用字面量语法创建其对象。要想创建自定义子类的实例,必须采用“非字面量语法”(nonliteral syntax)。

使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,则需要复制一份:

1
NSMutableArray *mutable = [@[@1, @2, @3, @4] mutableCopy];

这么做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多于上述缺点的。

用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。

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

编写代码时经常要定义常量。掌握了Objective-C与其C语言的基础的人,也许会用这种方法来做:

1
#define ANIMATION_DURATION 0.3

上述预处理指令会把源代码中的ANIMATION_DURATION字符串替换为0.3.预处理过程会把碰到的所有ANIMATION_DURATION一律替换成0.3,这样的话,假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。

要解决此问题,应该设法利用编译器的某些特性才对。

1
static const NSTimeInterval kAnimationDuration = 0.3;

用此方式定义的常量包含类型信息,其好处的清楚地描述了常量的含义。

常用的命名法是:

  • 若常量局限于某”编译单元”(translation unit,也就是“实现文件”,implementation file)之内,则在前面加字母k;
  • 若常量在类之外可见,则通常以类名为前缀。

定义常量的位置很重要。在头文件里声明预处理指令,这样会增加常量名称互相冲突的可能性。

在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。

枚举使用

枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)。

1
2
3
4
5
enum IHConnectionState {
IHConnectionStateDisconnected,
IHConnectionStateConnecting,
IHConnectionStateConnected
};

默认情况下,枚举起始值为0,以后依次递增,1,2,3…

其实还可以我们自己指定枚举值:

1
2
3
4
5
enum IHConnectionState {
IHConnectionStateDisconnected = 1,
IHConnectionStateConnecting,
IHConnectionStateConnected
};

也可以定义为位移值:

1
2
3
4
5
6
7
8
9
enum UIViewAutoresizing {
UIViewAutoresizing = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

关于枚举,Foundation框架中定义了一些辅助的宏,用这些来定义枚举类型时,也可以指定用于保存枚举值的底层数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
typedef NS_ENUM(NSUInteger, IHConnectionState) {
IHConnectionStateDisconnected = 1,
IHConnectionStateConnecting,
IHConnectionStateConnected
};

typedef NS_OPTIONS(NSUInteger, IHPermittedDirection) {
IHPermittedDirectionUp = 1 << 0,
IHPermittedDirectionDown = 1 << 1,
IHPermittedDirectionLeft = 1 << 2,
IHPermittedDirectionRight = 1 << 3
};

这些宏的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if(__cplusplus && __cplusplus >= 201103L && (__has_extension(cxx_strong_enums) || __has_feature(objc_fixed_enum))) || (!__cplusplus && __has_feature(objc_fixed_enum))

#define NS_ENUM(_type, _name)
enum _name:_type _name; enum _name:_type
#if (__cplusplus)
#define NS_OPTIONS(_type, _name)
type _name; enum:_type
#else
#define NS_OPTIONS(_type, _name)
enum _name:_type _name; enum _name:_type
#endif
#else
#define NS_ENUM(_type, _name) _type _name; enum
#define NS_OPTIONS(_type, _name) _type _name; enum
#endif

第一个#if用于判断编译器是否支持新式枚举。如果不支持,那么就用老式语法来定义枚举。

在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。