熟悉Objective-C
第1条:了解Objective-C语言的起源
- Objective-C为C语言添加了面向对象特性,是其超集。理解C语言的内存模型(memory model)有助于理解Objective-C的内存模型及其引用计数(reference counting)机制的工作原理。
- Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。在接收一条消息之后,究竟应执行何种代码,有运行期环境决定,而非编译器决定。
第2条:在类的头文件中尽量少引入其他头文件
-
当我们在编译一个类的时候,不需要知道该类的全部细节,只需要知道有一个这样的类就好的时候,我们就不用通过#import的方式来引入该类头文件。
这叫“向前声明”(forward declaring)该类。
@class "EOCEmployer.h"
import和@class的区别
import会包含这个类的所有信息,包括实体变量和方法,而@class只是告诉编译器,其后面声明的名称是类的名称。
在头文件中, 一般只需要知道被引用的类的名称就可以了。不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。
而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
如果存在循环依赖关系,如:A -> B, B -> A这样的相互依赖关系,当使用#import来相互包含,那么就会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。
所以,一般来说,@class是放在interface中的,只是为了在interface中引用这个类,把这个类作为一个类型来用的。在实现这个接口的实现类中,如果需要引用这个类的实体变量或者方法之类的,还是需要import。
-
当我们需要声明某个类遵循一项协议时,不能使用@class。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其通过#import的方式引入。
第3条:多用字面量语法,少用与之等价的方法
- 使用字面量语法来声明可以缩减源代码长度,使其更为易读。
字面数值
NSNumber *Number = [NSNumber numberWithInt:1];
NSNumber *newNumber = @1;
字面数组
如果数组元素对象中有nil,则会抛出异常,因为字面量语法实际上只是一种“语法糖”(syntactic sugar),其效果等于是先创建了一个数组,然后把方括号内的所有对象都加入到这个数组中。
因此,如下面这两个数组的第二位都未nil时,array里面会只有@“one”一个对象,而newArray将会抛出异常。这种微妙的差别表明,使用字面量语法更为安全。抛出异常令程序中止执行,这比创建好数组之后才发现元素个数少了要好,因为向数组中插入nil通常就是说明程序有错,最终结果往往就是数组越界崩溃,而现在我们却可以通过异常可以更快地发现这个错误。
NSArray *array = [NSArray arrayWithObjects:@"one",@"two",@"three", nil];
NSArray *newArray = @[@"one",@"two",@"three"];
字面字典
与数组一样,用字面量语法创建字典时也有一个问题,那就是一旦有值为nil,便会抛出异常。
NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:
@"Mark",@"firstname",
@"Lin",@"lastname",nil];
NSDictionary *newDic = @{@"firstname" : @"Mark",
@"lastname" : @"Lin",
@"age" : @28};
大家可以很直观地看出两种创建实例变量的方法有什么区别,使用字面量的方式不仅可以令到代码看起来更加地整洁,而且字面量语法也更为精简,因为声明中只包含数值,而没有多余的语法成分。
局限性
字面量语法有一个小小的局限,就是除了字符串以外,所创建出来的对象必须属于Foundation框架才行。
而使用字面量语法创建的字符串、数组、字典都是不可变的(immutable),若想要可变版本的对象可以参考以下写法:
NSMutableArray *mutable = [@[@"one", @"two", @"three"] mutableCopy];
第4条:多用类型常量,少用#define预处理指令
-
编写代码时经常需要定义一些常量,很多人都会用下面这种方式来实现:
#define ANIMATION_DURATION 0.3
这种实现方式可能就是你想要的效果,但是这样定义出来的常量没有类型信息,此外,预处理过程会把所有ANIMATION_DURATION一律改成0.3,这样的话,假设此指令在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。
要想解决这个问题,可以使用下面这种方式来实现:
static const NSTimeInterval kAnimationDuration = 0.3;
此方式定义的常量包含类型信息,清晰描述常量的含义。而且定义常量的位置很重要,在头文件中声明预处理指令是一种很糟糕的做法,常量名称有可能互相冲突。因为Objective-C没有“名称空间”(namespace)这一概念,所以这样做等于声明了一个全局变量。
所以若不打算公开某常量,则应将其定义在使用该常量的实现文件里面,如下:
// MarkAnimatedView.h #import <UIKit/UIKit.h> @interface MarkAnimatedView : UIView - (void)animate; @end // MarkAnimatedView.m #import "MarkAnimatedView.h" static const NSTimeInterval kAnimationDuration = 0.3; @implementation MarkAnimatedView - (void)animate { [UIView animateWithDuration:kAnimationDuration animations:^{ //Perform animations }]; } @end
常量为什么一定要同时使用static与const来声明呢???
因为如果有人试图修改有const修饰符所声明的变量时,那编译就会报错,而这就是我们所需要达到的目的!!!而static修饰符则是意味着该常量仅在定义此常量的编译单元中可见。
编译器每收到一个编译单元,就会输出一份“目标文件”(object file),在Objective-C的语境下,“编译单元”一词通常指每个类的实现文件,因此在上述代码中声明的kAnimationDuration,其作用域仅限于由MarkAnimatedView.m所生成的目标文件中。
假如声明此常量时不加static,则编译器会为它创建一个“外部符号”(external symbol),此时如若另一个编译单元中声明了同名常量,则会报错。
而有时候确实是需要对外公开某个常量的时候,我们有应该如何处理呢?(如在类代码中调用NSNotificationCenter)
此类常量则应放在“全局符号表”(global symbol table)中,以便可以在定义常量的编译单元之外使用。具体实现如下:
```
// MarkAnimatedView.h
#import <UIKit/UIKit.h>
extern NSString *const MarkLoginNotification;
@interface MarkAnimatedView : UIView
- (void)animate;
@end
// MarkAnimatedView.m
#import "MarkAnimatedView.h"
static const NSTimeInterval kAnimationDuration = 0.3;
NSString *const MarkLoginNotification = @"MarkLoginNotification";
@implementation MarkAnimatedView
- (void)animate
{
[UIView animateWithDuration:kAnimationDuration animations:^{
//Perform animations
}];
}
- (void)doNotificationAction
{
[[NSNotificationCenter defaultCenter] postNotificationName:MarkLoginNotification object:nil];
}
@end
```
注意const修饰符在常量类型中的位置,常量定义应从右至左解读,所以在本例子中,MarkLoginNotification和kAnimationDuration就是“一个常量,而这个常量是指针,指向NSString对象”。
extern这个修饰符会告诉编译器,在全局符号表中将会有一个MarkLoginNotification的符号,编译器无须查看其定义,因为它知道当链接成二进制文件之后,肯定能找到这个常量。
这样定义常量要优于#define预处理指令,因为编译器会确保常量值不变,而且一旦定义好之后,即可随处使用。
第5条:用枚举表示状态、选项、状态码
-
枚举只是一种常量命名方式, 使用枚举来表示各种状态码可以便于程序猿们更好地去理解代码。如使用枚举来表示“套接字连接”(socket connection)的状态:
enum MarkConnectionState { MarkConnectionDisconnected, MarkConnectionConnecting, MarkConnectionConnected, };
-
要想每次不用敲入enum而只需写MarkConnectionState的话,则需要使用typedef关键字重新定义枚举类型:
enum MarkConnectionState { MarkConnectionDisconnected, MarkConnectionConnecting, MarkConnectionConnected, }; typedef enum MarkConnectionState MarkConnectionState;
现在就可以使用简写的MarkConnectionState来代替完整的enum MarkConnectionState了:
MarkConnectionState state = MarkConnectionConnected;
-
C++11标准修订了枚举的某些特性,其中就包括了很重要的一点:可以指明使用何种“底层数据类型”(underlying type)来保存枚举类型的变量。这样做的好处就是可以向前声明枚举变量了:
enum MarkConnectionState : NSInteger;
当然,还可以不使用编译器所分配的序号,而手工指定某个枚举成员所对应的值:
enum MarkConnectionState { MarkConnectionDisconnected = 22, MarkConnectionConnecting, MarkConnectionConnected, };
如前所述,由于第一个枚举值为22,所以接下来的几个枚举值都会在上一个的基础上递增1。
-
还有一种情况是非常推荐使用枚举类型的,那就是-----定义选项!
enum MarkAutoresizing { MarkAutoresizin = 0, MarkAutoresizinWidth = 1 << 0, MarkAutoresizinHeight = 1 << 1, MarkAutoresizinTop = 1 << 2, MarkAutoresizinBottom = 1 << 3, };
-
在iOS UI框架中的UIKit里也有一个非常常见的例子,那就是用枚举值来告诉系统视图所支持的设备显示方向,这个枚举类型叫做UIInterfaceOrientationMask,开发者可以通过supported InterfaceOrientations的方法,将视图所支持的显示方向告诉系统。
-
最后再讲一种枚举的用法,在swith中使用枚举:
typedef NS_ENUM(NSUInteger, MarkConnectionState){ MarkConnectionDisconnected, MarkConnectionConnecting, MarkConnectionConnected, }; switch (_currentState) { case MarkConnectionDisconnected: break; case MarkConnectionConnecting: break; case MarkConnectionConnected: break; default: break; }
使用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型,这样做可以确保枚举是用开发者所选的底层数据类型实现出来,而不会采用编译器所选的类型。
在处理枚举类型的swith语句中不要实现default分支,这样就可以在加入新枚举之后,编译器自动提示开发者:swith语句并未处理所有枚举。