Objective-C 回顾【一】之 熟悉 Objective-C

Objective-C 语言的起源


Objective-C 与 C++,Java 等面向对象的语言类似,不过在很多地方还是有所差别。Objective-C 使用“消息结构”(messaging structure)而非“函数调用”(function calling)。

消息结构的语言与函数调用的语言关键区别在于:

  • 使用消息结构的语言,其运行时所应执行的代码由运行环境决定,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定;
  • 使用函数调用的语言,运行时所执行的代码由编译器决定;

要点:

  • Objective-C 为 C 语言添加了面向对象特性,是其超集。Objective-C 使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
  • 理解 C 语言的核心概念有助于写好 Objective-C 程序。尤其要掌握内存模型与指针。

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


在类的头文件中(.h)我们一般不需要知道引入的某个类的全部细节,这时候,我们可以用 @class 关键字去告诉编译器,知道有一个类名为 xxx 的类就好,不需要关注细节。

这叫做“向前声明”(forward declaring)该类。在实现文件(.m)中我们使用该类时就需要知道其所有细节,这时我们需要用 #import 关键字去导入 xxx 类的头文件。

将引入头文件的时机尽量延后,只有确有需要时才引入,这样就能减少类的使用者所需引入头文件的数量。这样能够一定程度上的减少编译的时间

向前声明也解决了两个类相互引用的问题。如果 A 类的头文件中引用了 B 类,B 类的头文件中又引用了 A 类,那么意味着这两个类里有一个无法被正确编译。

但是在写继承和协议的时候,又不能避免的要在头文件中引用其他的头文件。最好的解决方式就是将协议放在一个单独的头文件中,这样就能避免如果协议是放在一个很大的头文件中的话,就需要引用那个头文件中的所有内容。这样不仅可能产生相互依赖的问题,还有可能会增加编译时间。

要点:

  • 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
  • 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

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


要点:

  • 应该使用字面量语法来创建字符串,数值,数组,字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
  • 应该通过取下标操作来访问数组下标或字典中的键所以对应的元素。
  • 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。

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


我们在写动画的时候,很多人喜欢将动画的持续时间常量写成预处理指令的形式,如下:

1
#define ANIMATION_DURATION 0.3

这样做并不算错,但是有两个问题:

  • 定义出来的常量没有类型信息。
  • 预处理指令过程会把碰到的所有 ANIMATION_DURATION 一律替换成 0.3,这样的话,假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码,其 ANIMATION_DURATION 都会被替换掉。

改成下面的方式就会更好一点:

1
static const NSTimeInterval kAnimationDuration = 0.3;

这里有个命名习惯:

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

变量一定要同时用 staticconst 来声明。如果试图修改由 const 修饰符所声明的变量,那么编译器就会报错。而 static 修饰符则意味着该变量仅在定义此变量的编译单元中可见。在 Objective-C 中”编译单元”一词通常指每个类的实现文件(以 .m 为后缀名)。如果不加 static,则编译器会为它创建一个“外部符号”(external symbol)。此时若是另一个编译单元中也声明了同名变量,那么编译器就抛出一条错误信息:

1
2
3
duplicate symbol _kAnimationDuration in
xxx.o
aaa.o

实际上,如果一个变量既声明为 static,又声明为 const,那么编译器根本不会创建符号,而是会像 #define 预处理指令一样,把所有遇到的变量都替换为常量。不过还是要记住:用这种方式定义的常量带有类型信息。

如果需要对外公开某个常量。需要将常量放在“全局符号表”(global symbol table)中,以便可以在定义该常量的编译单元之外使用。应该这样来定义:

1
2
3
4
5
// 头文件中
extern NSString *const xxx;

// 实现文件中
NSString *const xxx = x;

extern 关键字是告诉编译器,在全局符号表中将会有一个名叫 xxx 的符号。也就是说,编译器无须查看其定义,即允许代码使用此常量。因为它知道,当链接成二进制文件之后,肯定能找到这个常量(如果不在实现文件中定义,编译器将会报错,二进制文件中找不到该常量)。

要点:

  • 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
  • 在实现文件中使用 static const 来定义“只在编译单元内可见的常量”(translation unit specific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
  • 在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常与之相关的类名做前缀。

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


要点:

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