第1条:了解Objective-C语言的起源
Objective-C
语言由Smalltalk
1演化而来的,Smalltalk
是消息型语言的鼻祖,所以OC
使用的是“消息结构”(messaging structure)而非“函数调用”(function calling)。
1.消息与函数调用之间的区别:
//Messaging(Objevtive-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
代码时依然适用。所以要想写出高效的OC
代码就得完全掌握OC
和C
这两门语言,其中尤为重要的是要理解C语言
的内存模型(memory medel),这有助于我们理解OC
的内存模型和“引用计数”(reference counting)机制的工作原理。
2.OC的内存管理:
NSString stackString;
//error:interface type cannot be statically allocated
NSString *someString = @"The string";
因为OC
声明变量基本上都为指针变量,所以OC
对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。
对象分配在栈上,而实例分配在堆中。
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。
3.要点:
Objective-C
为C语言
添加了面向对象特性,是其超集。Objective-C
使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。- 理解
C语言
的核心概念有助于写好Objective-C
程序。尤其是要掌握内存模型和指针。
第2条:在类的头文件中尽量少引入其他头文件
与C
和C++
一样,Objective-C
也使用“头文件”(header file)与“实现文件”(implementation file)来区隔代码。
1.用Objective-C编写“类”(class)的标准方式:
以类名做文件名,分别创建两个文件,头文件后缀用.h
,实现文件后缀用.m
。
//EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@end
//EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
//Implementation of methods
@end
2.在一个文件中引入另一个文件(向前声明):
通常我们在一个文件中要引入另一个文件我们就需要在其加入另一个文件的头文件即.h
文件,如下:
//EOCPerson.h
#import <Foundation/Foundation.h>
#import "EOCEmployer.h"
@interface EOCPerson : NSObject
@end
//EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
//Implementation of methods
@end
这种方法可行,但是不够优雅。我们不需要让其知道EOCEmployer类
的全部细节,只需要让其知道有一个这样的类就行,所以可以采用下面的方法:
//EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer;
@interface EOCPerson : NSObject
@end
//EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
//Implementation of methods
@end
这叫做“向前声明”(forward declaring)该类。
注意: 在.h
文件中因为只需要有这个类就行所以可以只使用@class EOCEmployer;
添加,但是在.m
实现文件中因为要用到该文件的具体细节内容,所以要加上#import "EOCEmployer.h"
头文件。
将引入头文件的时机尽量延后,只在需要的时候才引入,这样就可以减少类的使用者所需引入头文件的数量。
3.向前声明的好处:
向前声明也解决了两个类相互引用的问题。
例如:有两个类,它们都在头文件中引入了对方的头文件,两个类都进行各自的引用解析,这样就会导致“循环引用”(chicken-and-egg situation)。虽然我们使用#import
而非#include
不会导致死循环,但是这意味着两个类中有一个类无法被正确编译。
但是,有时候就必须引入头文件,比如继承以及遵循的协议。
4.要点:
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循的协议”的这条声明移至“
class-continuation分类
”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
第3条:多用字面量语法,少用与之等价的方法
字面量语法说白了就是不用系统给的初始化方法,而是直接对一个变量进行赋值,就和C语言相似,比如:
当然也可以用这种语法来声明NSNumber、NSArray、NSDictionary类的实例,并且使用这种语法可以缩减源代码长度,使其更为易读。
1.字面数值:
原来我们对NSNumber类进行初始化时采取系统的初始化方法:
而使用字面量语法可以直接进行赋值:
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
//对运算也适用
int x = 5;
float y = 6.32f;
NSNumber *expressionNumber = @(x * y);
由此可以看出字面量语法非常的简洁,没有任何多余的语法成分。
2.字面量数组:
之前创建一个数组:
而使用字面量语法来创建:
上面的创建方法不仅简单而且还利于操作数组,就比如访问数组的元素,之前是:
使用字面量就可以直接:
这也叫做“取下标”操作(subscripting),跟其他的也相同,这种方式更加简洁、更易理解。
注意: 用字面量创建数组时要注意,若数组元素对象中有nil,则会抛出异常,因为字面量语法实际上是一种“语法糖”(syntactic sugar)1,其效果相当于是先创建了一个数组,然后把方括号里的所有对象都添加到这个数组中,然而使用自带的初始化方法,它是当遇到nil时就会停止返回,所以我们可以利用这个特性来判断数组初始化是否正确。
1:也称“糖衣语法”,是指计算机语言中与另外一套语法等效但是开发者用起来却更加方便的语法。语法糖可令程序更易读,减少代码的出错几率。
3.字面量字典:
官方初始化字典变量,两两一对,<对象>,<键>:
这样写与我们通常理解的模式不太相同,理解起来可能会有点麻烦,所以我们可以使用字面量定义:
这样写我们理解起来就简单的多了,并且这个与数组相同,只要遇到nil
就会抛出异常,这有助于查错。
当然字典变量的访问也可以使用字面量方法:
这样写也省去了冗赘的语法,令此代码更简单易读。
4.可变数组与字典:
通过取下标操作,可以访问数组中的某个元素或者字典中的某个键对应的元素,如果数组和字典是可变的(mutable),那么也能通过下标修改其中的元素值,例如:
mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";
5.局限性:
字面量语法有个小小的限制,就是除了字符串以外,所创建出来的对象必须属于Foundation框架
才行。如果自定义了这些类的子类,则无法用字面量语法创建其对象。
6.要点:
- 应该使用字面量语法来创建字符串、数值、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或者字典中的键对应的元素。
- 用字面量语法创建数组或者字典时,若值中有
nil
,则会抛出异常。因此,务必确保值里不含nil
。
第4条:多用类型常量,少用#define预处理指令
通常我们在写程序的时候都会使用#define
来定义一个固定的数据,方便我们后续自己的编写,但是这样定义出来的常量没有类型信息,并且假设此命令在某个头文件中,那么所有引入了这个头文件的的代码,其定义的固定值都会被这个替换掉,反而破坏了程序。
那么这个时候我们就可以使用下面的方法:
这种方式定义的常量包含类型信息,其好处是清楚的描述了常量的含义。由此可知,该常量类型为NSTimeInterval
,这有助于其编写开发文档。
1.常量常用的命名法:
若常量局限于某“编译单元”(也就是“实现文件”)之内,则在前面加字母k
;若常量在类之外可见,则通常以类名为前缀。
2.常量的定义位置:
常量定义的位置也非常重要,我们最好不要将常量定义在头文件中,若你定义在头文件中,又被其他的文件引用了,那么该这个文件中的这个常量都会被其替换掉,所以最好不要在头文件中定义常量,不论你是如何定义常量的,因为OC中没有“名称空间”这一概念。
3.使用static const而不用#define的原因:
使用static const
来声明一个常量,如果试图修改由const
修饰符所声明的变量,那么编译器就会报错。static
修饰符则意味着该变量仅在定义此变量的编译单元中可见。在OC
中“编译单元”通常指每个类的实现文件(.m
文件)。而#define不具有这些功能,所以我们使用static const来修饰。
假如声明此变量时不加static
,则编译器会为它创建一个“外部符号”。此时若是另一个编译单元中也声明了同名变量,那么编译器就会抛出错误信息。
4.定义一个全局常量:
有时候我们需要对外公开我们的常量,比如说是通知时的通知名称,我们定义一个常量,外界就可以直接使用这个常值变量来注册自己想要接收的通知即可,而不用知道实际字符串的值。
此类常量需放在“全局符号表”中,以便可以在定义该常量的编译单元之外使用。举例说明:
//In the header file
extern NSString *const EOCStringConstant;
//In the implementation file
NSString *const EOCStringConstant = @"VALUE";
因为常量定义应该从右向左解读,所以它是一个不可变的指向NSString *
类型的指针,这个指针的指向不允许被改变。
extern
就是告诉编译器,在全局符号表中将会有一个名叫EOCStringConstant
的符号,也就是说,编译器无需查看其定义,即允许代码使用此常量。
又因为符号要放在全局符号表里,所以我们就得更加注意其命名,不能出现重名的情况,其名称应该严谨严谨再严谨!!!
5.要点:
- 不能用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const来定义“只在编译单元内可见的常量”。由于此常量不在全局符号表中,所以无需为其名称加前缀。
- 在头文件中使用extern来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应该加以区隔,通常用与之相关的类名做前缀。
第5条:用枚举表示状态、选项、状态码
枚举在我们写iOS程序时十分的常见,比如按钮的状态之类的,它都是使用枚举来定义的,我们相应的状态只能使用一种枚举状态,但是还有的情况我们可以选取多个枚举值,比如获取通知传值的新值和旧值这就是一个多状态的枚举。
1.单项枚举:
单项枚举通常都是使用数字作为编号来逐一增加编号的值来确定的,默认的编号从0开始,比如:
enum EOCConnectionState {
EOCConnectionStateDisConnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
这里的EOCConnectionStateDisConnected
就为0,而EOCConnectionStateConnected
就是2了。当然你也可以自行去定其枚举的初始编号:
enum EOCConnectionState {
EOCConnectionStateDisConnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
但是,通常使用枚举的话定义变量的类型太长了,而且不太简洁:
这时候我们就可以使用typedef
来重新命名:
这样EOCConnectionState
就代表了之前的enum EOCConnectionState
就简洁了很多了。
除了这些,编译器还更新了指定底层的数据类型,所用的语法是:
这样这个枚举类型数据的底层数据类型就是NSInteger
了,我们也可以使用向前声明的方法:
2.多项枚举:
多项枚举其实就是使用2的幂次方
来表示一个枚举数值,对底层二进制了解的程序员可能更容易理解。所以这里的多项枚举其实就是在其二进制的特殊性上来演变出来的,通过“按位或操作”将选择的枚举直接进行或运算得到一个得数,通过这个得数就可以确定用户选择的状态。例如:
enum UIViewAutoresizing {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5,
};
这里的每个选项均可启用或者禁用,例如:
enum UIViewAutoresizing resizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (resizing & UIViewAutoresizingFlexibleWidth) {
//UIViewAutoresizingFlexibleWidth is set
}
它的原理其实就是这样:
当然多项枚举和单项枚举也一样,都可以自行确定枚举类型数据的底层数据类型,与上述写法相同。
3.与Switch语句的结合:
typedef NS_ENUM (NSUInteger, EOCConnectionState) {
EOCConnectionStateDisConnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
switch (_currentState) {
EOCConnectionStateDisConnected:
//Handle disconnected state
break;
EOCConnectionStateConnecting:
//Handle connecting state
break;
EOCConnectionStateConnected:
//Handle connected state
break;
}
通常我们喜欢在switch
语句的最后加上default
分支,但是这里不要用!!!因为你本就是枚举变量,使用default
分支如果用户没有使用枚举中的类型,那么这个switch
语句也会进入default
分支,就与我们的本意不符了,还会造成程序的错误,所以这里不要用default
分支!!!
4.要点:
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
- 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
- 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。