【Effective Objective - C】—— 熟悉Objective-C
- 熟悉Objective-C
- 1.oc的起源
- 消息和函数的区别
- 运行期组件和内存管理
- 要点:
- 2.在类的头文件中尽量少引入其他头文件
- 向前声明
- 要点:
- 3.多使用字面量语法,少用与之等价的方法
- 字符串字面量
- 字面数值
- 字面数组
- 字面字典
- 局限性
- 要点
- 4.多用类型常量,少用#define预处理指令
- 常量的名称与位置
- 常量命名法
- 位置
- 使用static与const来声明
- static修饰符
- const修饰符
- 使用extern声明全局变量
- 要点
- 5.用枚举表示状态,选项,状态码
- 要点
熟悉Objective-C
Objective-C通过一套全新语法,在C语言基础上添加了面向对象特性。Objective-C的语法中频繁使用方括号,而且不吝于写出极长的方法名,这通常令许多人觉得此语言较为冗长。其实这样写出来的代码十分易读,只是C++或Java程序员不太能适应。
Objective-C语言学起来很快,但有很多微妙细节需注意,而且还有许多容易为人所忽视的特性。另一方面,有些开发者并未完全理解或是容易滥用某些特性,导致写出来的代码难于维护且不易调试。本章讲解基础知识,后续各章谈论语言及其相关框架中的各个特定话题。
1.oc的起源
和C++,Java一样,Objective-C也是面向对象语言,但是它们在许多方面都有差别。差别在于Objective-C使用的是消息结构而非函数调用,Objective-C语言由Smalltalk演化而来的,Smalltalk是消息型语言的鼻祖
消息和函数的区别
//Messaging
Object* obj = [Object new];
[obj performWith: parameter1 and: parameter2];
//Function
Object* obj = new Object;
obj->preform (parameter1, parameter2);
关键区别在于:使用消息结构的语言,其运行时所应执行的代码由环境来决定;使用函数调用的语言,则由编译器决定。对于函数来说,如果范例代码的调用函数是多态的,那么就在运行时按照虚方法表来查出来到底执行哪个函数,而采用消息结构的语言,不管是否为多态总是在运行时才回去查找所要执行的方法。
运行期组件和内存管理
- Objective-C的重要工作都由“运行期组件”(runtime component)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说,运行期组件中含有全部内存管理方法。运行期组件本质上就是一种与开发者所编代码相链接的“动态库”(dynamic library),其代码能把开发者编写的所有程序粘合起来。这样的话,只需更新运行期组件,即可提升应用程序性能。而那种许多工作都在“编译期”(compile time)完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。
- Objective-C是C的“超集”(superset),所以C语言中的所有功能在编写Objective-C代码时依然适用。因此,必须同时掌握C与Objective-C这两门语言的核心概念,方能写出高效的Objective-C代码来。其中尤为重要的是要理解C语言的内存模型(memory model),这有助于理解Objective-C的内存模型及其“引用计数”(reference counting)机制的工作原理。若要理解内存模型,则需明白:Objective-C语言中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用如下语法:
NSString *someString = @"The string";
上述代码声明了一个someString的变量,类型为Nsstring*,也就是说,此变量为指向Nsstring的指针,所有oc的对象都必须这样声明,对象所占内存总是分配在堆空间上,而绝不能分配在栈空间上。
如果再次创建一个对象Same,那么这两个对象队徽分配在堆中,它们同时指向了堆中的NSString实例:
NSString *someString = @"The string";
NSString *anotherString = someString;
如下图所示:
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈桢弹出时自动清理。
Objective-C运行期环境把堆内存管理工作抽象为一套内存管理结构,名叫“引用计数”。
要点:
- Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 理解C语言的核心概念有助于写好Objective-C程序。尤其是要掌握内存模型和指针。
2.在类的头文件中尽量少引入其他头文件
Objective-C采用的是头文件和实例文件区分代码,头文件.h ,实例文件.m,这里以EOCPerson为例格式如下:
// EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@end
//EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
// Implementation of methods
@end
向前声明
如果又创建一个名为EOCEmployer的新类,然后为EOCPerson类添加这个属性。就会是这个样子。
// EOCPerson.h
#import <Foundation/Foundation.h>
#import "EOCEmployer.h"
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
如果在EOCPerson类的头文件中,我们不需要知道这个新类的全部信息,就可以使用向前声明的方式。现在的头文件就变成这样了:
// EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer;
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
EOCPerson类的实现文件则需引入EOCEmployer类的头文件,因为若要使用后者,则必须知道其所有接口细节。于是,实现文件就是:
//EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
// Implementation of methods
@end
尽量将引入头文件的时机延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入头文件数量,减少编译时间。
向前上名声明的好处:
- 解决了这两个类相互引用问题。
- 相互引用:有两个类,它们都在头文件中引入了对方的头文件,两个类都进行各自的引用解析,这样就会导致“循环引用”(chicken-and-egg situation)。虽然我们使用#import而非#include不会导致死循环,但是这意味着两个类中有一个类无法被正确编译。
- 但是,有时候就必须引入头文件,比如继承以及遵循的协议。
要点:
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循的协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入,减少不必要的编译,提升性能。
3.多使用字面量语法,少用与之等价的方法
字符串字面量
不使用alloc及init方法来分配并初始化NSString对象,让语法更简洁。
NSString *someString = @"Effective Objective-C 2.0";
这种语法也可以来声明NSNumber、NSArray、NSDictionary类的实例。
字面数值
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
字面数组
字面量语法创建数组。
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
字面量语法操作数组
NSString *dog = animals[1];
字面字典
“字典”是一种映射型数据结构,可向其中添加键值对。
字面量字典创建
NSDictionary *personData = @{@"firstName" : @"Matt", @"lastName" : @"Galloway", @"age" : @28};
字面量语法访问
NSString *lastName = personData[@"lastName"];
这样写省去了沉赘的语法,令此行代码简单易读。
局限性
字面量语法除了字符串以外,所创建出来的对象必须属于Foundation框架才行。然而一般来说,标准的实现已经很好了,使用这些已经足够了。
此外,使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,,则需复制一份:
NSMutableArray *mutable = [@[@1, @2, @3, @4] mutableCopy];
这样做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多与上述缺点的。
要点
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
4.多用类型常量,少用#define预处理指令
编写代码时经常要定义常量。如果我们使用预处理指令,如下。
#define ANIMATION_DURATTON 0.3;
那么源代码中的ANIMATION_DURATTON字符串都会被替换为0.3,不过这样定义出的常量没有类型信息。此外,假设此指令声明在某个头文件中,那么所有引入这个头文件的代码,其ANIMATION_DURATTON都会被替换。
所以我们最好使用类型常量,如下:
static const NSTimeInterval KAnimationDuration = 0.3;
用此方法定义的常量包含类型信息,其好处是清楚地描述了常量的含义。由此可知该常量类型为NSTimeInterval,这有助于为其编写开发文档。
常量的名称与位置
常量命名法
若常量局限于某“编译单元”(也就是“实现文件”)之内,则在前面加字面k;若常量在类之外可见,则通常以类名为前缀。
位置
位置
因为Objective-C没有“名称空间”(namespace)这一概念,所以在头文件使用static const定义常量,其实等于声明了一个名叫KAnimationDuration的全局变量。此名称应该加上前缀,以表明其所属的类,例如可改为EOCViewClassAnimationDuration。
若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。
// EOCAnimatedView.h
#import <UIKit/UIKit.h>
@interface EOCAnimatedView : UIView
- (void)animate;
@end
// EOCAnimatedView.m
#import "EOCAnimatedView.h"
static const NSTimeInterval KAnimationDuration = 0.3;
@implementation EOCAnimatedView
- (void)animate {
[UIViewanimateWithDuration:KAnimationDuration animations:^(){
// .......
}];
}
@end
使用static与const来声明
static修饰符
该修饰符意味着变量仅在定义此变量的编译单元可见。假如声明此变量时不加static,则编译器会为它创建一个“外部符号”。此时若是另一个编译单元中也声明了同名变量,那么编译器就会抛出一条错误消息:
duplicate symbol _KAnimationDuration in:
EOCAnimatedView.o
EOCOtherView.o
const修饰符
该变量意味着变量不可修改,如果试图修改由const修饰符所声明的变量,那么编译器就会报错。
实际上,如果一个变量既声明为static,又声明为const,那么编译器就会像#define预处理指令一样,把所有遇到的变量都替换为常值。不过,用这种方式定义的常量带又类型信息。
使用extern声明全局变量
有时候需要对外公开某个常量。此时,我们需要声明一个外界可见的常值变量(constant variable)。此类常量需放在“全局符号表”(global symbol table)中,以便可以在编译单元之外使用。定义方法为:
// In the header file
extern NSString *const EOCStringConstant;
// In the implementation file
NSString *const EOCtringCostant = @"VALUE";
这个常量在头文件中“声明”,且在实现文件中“定义”。在本例中,EOCtringCostant就是一个常量,这个常量是指针,指向NSString对象。
此类常量必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件里。
要点
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
- 在头文件中使用extern来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
5.用枚举表示状态,选项,状态码
枚举只是一种常量命名方式,某个对象所经历的各个状态、定义选项或者把逻辑含义相似的一组状态码都可以放入一个枚举集里。
编译器会为枚举分配一个独有的编号,从0开始,每个枚举递增1,也可以手动设置某个枚举成员对应的值,后面的枚举值一次加1。
可以指明枚举用的何种底层数据类型,这样编译器清楚底层数据类型的大小,可以向前声明枚举类型。
UIButton的状态:
typedef NS_ENUM(NSInteger, UIButtonRole) {
UIButtonRoleNormal,
UIButtonRolePrimary,
UIButtonRoleCancel,
UIButtonRoleDestructive
} API_AVAILABLE(ios(14.0));
要点
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
- 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
- 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所有枚举。