Objective-C使用“消息结构”而非“函数调用”。使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。
Objective-C是C的“超集”(superset)。
OC中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可以:
NSString *someString = @"The string";
所有OC语言的对象都必须这样声明,因为对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。不能在栈中分配OC对象:
NSSring stackString;
// error: interface type cannot be statically allocated
someString
变量指向分配在堆里的某块内存,其中含有一个NSString
对象。也就是说,如果再创建一个变量,令其指向同一地址,那么并不拷贝该对象,只是这两个变量会同时指向该对象:
NSString *someString = @"The string";
NSString *anotherString = someString;
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧(stack frame)弹出时自动清理。
OC将堆内存管理抽象出来了,不需要malloc
及free
来分配或释放对象所占内存。OC运行期环境把这部分工作抽象为一套内存管理架构,叫做“引用计数”。
定义里不含*的变量,它们可能会使用“栈空间”(stack space)。这些变量所保存的不是OC对象。比如CoreGraphics框架中的CGRect:
CGRect frame;
frame.origin.x = 0.0f;
...
CGRect是C结构体,其定义:
struct CGRect {
CGPoint origin;
CGSize size;
}
typedef struct CGRect CGRect;
改用OC对象,性能会受影响。
一、在类的头文件中尽量少引入其他头文件
例:
//EOCPerson.h
#import <Foundation/Foundation.h>
//#import <EOCEmployer.h>
//不在此引入头文件
@class EOCEmployer;
//使用向前声明(forward declaring)
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
EOCPerson
的实现文件则要引入EOCEmployer
类的头文件,因为需要知道其所有接口细节来使用它。
将引入头文件的时机尽量延后,只在确有需要时引入,这样可以减少类的使用者所引入头文件的数量。否则会增加编译时间。
向前声明也解决了两个类相互引用的问题。假设要为EOCEmployer类加入新增及删除雇员的方法,那么其头文件会加入如下定义:
- (void)addEmployee:(EOCPerson*)person;
- (void)removeEmployee:(EOCPerson*)person;
如果在各自头文件中引入对方头文件,则会导致循环引用(chicken-and-egg situation)。这会导致两个类里有一个无法被正确编译。
有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的声明移至“class-continuation分类”中,如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
二、多用字面量语法,少用与之等价的方法
字面数值:
NSNumber *someNumber = [NSNumber numberWithInt: 1];
//下面的代码更加整洁:
NSNumber *someNumber2 = @1;
//此外,其他数据类型也可以使用字面量语法:
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.14159;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
//也适用于:
int x = 5;
float y = 6.32f;
NSNumber *expressionNumber = @(x * y);
字面量数组:
NSArray *animals = [NSArray arrayWithObjects: @"cat", @"dog", @"mouse", @"badger", nil];
//字面量语法:
NSArray *animals2 = @[@"cat", @"dog", @"mouse", @"badger"];
NSString *dog = [animals objectAtindex: 1];
NSString *dog2 = animals[1];
使用字面量语法创建数组时,若数组元素对象中有nil,会抛出异常。arrayWithObjects:方法会依次处理各个参数,直到发现nil为止。这个差别说明,使用字面量语法更安全,抛出异常令程序终止执行,这比创建好数组后才发现元素个数少了要好。向数组中插入nil通常说明程序有错,而通过异常可以更快地发现这个错误。
字面量字典:
NSDictionary *personData = [NSDictionary dictionaryWithObjects:
@"Matt", @"firstName",
@"Galloway", @"lastName",
[NSNumber numberWithInt: 28], @"age",
nil];
NSDictionary *personData2 =
@{@"Matt", @"firstName",
@"Galloway", @"lastName",
@"age" : @28};
//与数组一样,用字面量语法创建字典时也有个问题,那就是一旦有值为nil,便会抛出异常。
NSString *lastName = [personData objectForKey:@"lastName"];
NSSrting *lastName2 = personData[@"lastName"];
可变数组与字典:
[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"Galloway" forKey:@"lastName"];
mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";
局限:
想创建自定义子类的实例,必须采用“非字面量语法”。
使用字面量创建的字符串、数组、字典对象都是不可变类型。若要可变版本,则需要复制一份:
NSMutableArray *mutable = [@[@1, @2, @3]mutableCopy];
三、多用类型常量,少用#define预处理指令
#define ANIMATION_DURATION 0.3
这样做,会使定义出来的常量没有类型信息,此外,预处理过程会把所有的ANIMATION_DURATION
一律替换成0.3
。这样的话,假设它声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION
都会被替换。
因此,使用下面的方法就更好:
static const NSTimeInterval kAnimationDuration = 0.3;
此外,还要注意常量名:若常量局限于某个“编译单元”(即“实现文件”)之内,在前面加字母k;若常量在类外可见,则通常以类名为前缀。
变量一定要同时使用static
与const
来声明。const
表示不可修改,static
意味着该变量仅在此变量的编译单元内可见。
实际上,若一个变量既声明为static
,又声明const
,那么编译器不会创建该符号,而是会像#define
一样替换为常量。不过,这类常量含有类型信息。
有时候需要对外公开某些常量,比如使用NSNotificationCenter
通知他人。用一个对象来派发通知,令其他欲接受通知的对象向该对象注册,这样就能实现该功能了。派发通知时,需要用字符串来表示此项通知的名称,这个名称就可以声明为一个外界可见的常值变量。
此类常量需放在“全局符号表”中,以便可以在定义该常量的编译单元之外使用。应该这样定义:
//header file
extern NSString *const ECOStringConstant;
//implementation file
NSString *const ECOStringConstant = @"VALULE";
常量定义从右向左读:一个常量,这个常量是指针,指向NSString对象。此类常量必须要定义,而且只能定义一次。
为了避免名称冲突,最好使用与之相关的类名做前缀。例如:
//EOCAnimatedView.h
extern const NSTimeInterval EOCAnimatedViewAnimationDuration;
//EOCAnimatedView.m
const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3;
四、用枚举表示状态、选项、状态码
应该用枚举表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
如果把传递给某个方法的选项表示为枚举类型,而多个选项又可以同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
用NS_ENUM
与NS_OPTIONS
宏来定义枚举类型,并指明其底层数据类型。这样可以确保枚举是用开发者所选的底层数据类型实现出来的。而不会采用编译器所选的类型。
在处理枚举类型的switch
语句中不要实现default
分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。