一、了解 Objective-C 语言的起源
Objective-C 与 C++、Java 等类似,是一种面向对象的语言。该语言使用“消息结构”而非“函数调用”。Objective-C 语言由 Smalltalk 演化而来,后者是消息型语言的鼻祖。
Objctive-C 为 C 语言添加了面向对象的特性,是其超集。Objective-C 使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接受一条消息之后,究竟应该执行何种代码,由运行期环境而非编译器来决定。
二、在类的头文件中尽量少引用其他头文件
在类的头文件 .h 中,如果需要引用其它类,尽可能少用 #import
改而使用 @class
,在 实现文件 .m 中若要使用该类再使用 #import
。这样做是将引入头文件的时机尽量延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入的头文件数量,从而减少编译时间。
#import、#include、@class区别:
#include:
1、在C语言中,我们使用 #include 来引入头文件。
2、#include 会造成重复引用头文件。
3、为了防止重复引用可采用:
#ifndef ViewController_h #define ViewController_h #endif
#import:
import 是 include 的升级版,可以防止重复引入头文件这种现象的发生。
import会包含这个类的所有信息,包括实体变量和方法
使用 #import 头文件会自动只导入一次,不会重复导入,相当于#include和#pragma once
@class
@class 用来告诉编译器,有这样一个类,使书写代码时,不报错。 但是 @class 只是使导入的类名在引用时不受影响,不能创建该类的对象,因为创建对象时也需要访问其内部方法。
在编译效率方面考虑,如果你有100个头文件都 #import 了同一个头文件,或者这些文件是依次引用的,如 A–>B, B–>C, C–>D 这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用 @class 则不会。
但是也有一些情况,是不可避免要在 .h 里引用的。比如:继承某个类,必须在 .h 里 import 父类的 .h;类实现某个接口,必须在 .h 里引用接口的 .h 等等
三、多用字面量语法,少用与之等价的方法
字面量字符串:
NSString *someStr = @"someStr";
字面数值:
NSNumber *intNum = @1;
NSNumber *boolNum = @YES;
NSNumber *charNum = @'a';
字面量数组:
NSArray *animale = @[@"cat",@"dog",@"mouse"];
使用字面量数组的好处是,当数组元素对象中有 nil,则会抛出异常。因为字面量语法实际上是一种“语法糖”,其效果等于先是创建了一个数组,然后把方括号内的所有对象都加到这个数组中。
下面这段代码分别以两种语法创建数组:
NSArray *arrayA = [NSArray arrayWithObjects:obj1,obj2,obj3, nil];
NSArray *arrayB = @[obj1,obj2,obj3];
假如 obj1 与 obj3 都指向了有效的 Objective-C 对象,而 obj2 为 nil,则字面量语法创建的数组 arrayB 会抛出异常。而 arrayA 虽然能够创建出来,但是其中却只含有 obj1 一个对象。原因在于arrayWithObjects
方法会依次处理各个参数,之道发现 nil 为止,由于 obj2 是 nil,所以给方法会提前结束。
所以使用字面量语法更为安全,向数组中插入 nil 通常说明程序有错,而通过异常可以更快的发现这个错误。
字面量字典:
NSDictionary *personData = @{
@"name":@"matt",
@"age":@28
};
使用字面量语法创建出来的字符串、数组、字典对象都是不可变的,若想变成可变的版本的对象,则需复制一份:
NSMutableArray *mutable = @[@1,@2,@3].mutableCopy;
四、多用类型常量,少用 #define (宏)预处理指令
我们定义常量一般会使用宏 #define:
#define ANIMATION_DURATION 0.3
这样定义出来的常量不含类型信息,编译器只是会在编译前据此查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
在实现文件 .m 中我们应该使用类型常量来定义常量:
static const NSTimeInterval kAnimationDuration = 0.3;
在实现文件 .m 中,定义常量名称前面一般加字母 k。若在 .h 中,即常量在类之外可见,通常以类名为前缀:
//.h 中声明:
extern const NSTimeInterval EOCAnimationDuration;
//.m 中实现:
const NSTimeInterval EOCAnimationDuration = 1.0;
在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应该加以区隔,通常用与之相关的类名做前缀。
#define、const、static、extern区别
1、define 宏:
- 编译时刻:宏是预编译(编译之前处理),const是编译阶段。
- 编译检查:宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。
- 宏的好处:宏能定义一些函数,方法。 const不能。
- 宏的坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换。
2、const:
中文“常量”意思。
- const用来修饰右边的基本变量或指针变量。
- 被修饰的变量只读,不能被修改。
看下面的例子,相信你就完全理解 const 的用法:
int const *p // *p只读,p变量
int * const p // *p变量,p只读
const int * const p //p和*p都只读
int const * const p //p和*p都只读
3、static
中文“静态”意思。
(1)修饰局部变量
保证局部变量永远只初始化一次,在程序的运行过程中永远只有一份内存, 生命周期类似全局变量了,但是作用域不变。例如:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
static int i =0;
i++;
NSLog(@"i = %d",i);
}
得到的结果是变量 i 每次都会自增。如果不使用 static 修饰,则 i 每次结果只为 1。这就是关键字 static 修饰的局部变量的作用,让局部变量永远只初始化一次,一份内存,生命周期已经跟全局变量类似了,只是作用域不变。
(2)修饰全局变量
使全局变量的作用域仅限于当前文件内部,即当前文件内部才能访问该全局变量。
iOS 中在一个文件声明的全局变量,工程的其他文件也是能访问的,但是我又不想让其他文件访问,这时就可以用 static 修饰它了。
(3)修饰函数
static 修饰函数时,被修饰的函数被称为静态函数,使得外部文件无法访问这个函数,仅本文件可以访问。这个在 oc 语言开发中几乎很少用,c 语言倒是能看到一些影子,所以不详细探讨。
4、extern
中文“外部的”意思。它的作用是声明外部全局变量。这里需要特别注意extern只能声明,不能用于实现。
在开发中,我们通常会单独抽一个类来管理一些全局的变量或常量,下面来看看逼格比较高的一种做法:
我们可以在.h文件中 extern 声明一些全局的常量:
extern NSString * const name;
extern NSInteger const count;
.m 中实现
NSString * const name = @"王五";
NSInteger const count = 3;
这样,只要导入头文件,就可以全局的使用定义的变量或常量。
五、用枚举表示状态、选项、状态码
枚举是一种常量命名方式。
枚举表示状态(状态码同理)
//第一种写法,先定义枚举类型,再定义枚举变量
enum EOCConnetionState{
EOCConnetionStateDisconnected,
EOCConnetionStateConnecting,
EOCConnetionStateConnected
};
enum EOCConnetionState state;
//第二种写法,定义枚举类型的同时定义枚举变量
enum EOCConnetionState{
EOCConnetionStateDisconnected,
EOCConnetionStateConnecting,
EOCConnetionStateConnected
}state;
由于每种状态都用一个便于理解的值来表示,所以这样写出来的代码更易读懂。
枚举表示选项
一个“选项变量”的类型要能够同时表示一个或多个组合的选择,如下例子:
enum TTGDirection {
TTGDirectionNone = 0,
TTGDirectionTop = 1 << 0, //00000001
TTGDirectionLeft = 1 << 1, //00000010
TTGDirectionRight = 1 << 2, //00000100
TTGDirectionBottom = 1 << 3 //00001000
};
这里的选项是用位运算的方式定义的,这样的好处就是,我们的选项变量可以如下表示:
//用“或”运算同时赋值多个选项
enum TTGDirection direction = TTGDirectionTop | TTGDirectionLeft | TTGDirectionBottom;
//用“与”运算取出对应位
if (direction & TTGDirectionTop) {
NSLog(@"top");
}
if (direction & TTGDirectionLeft) {
NSLog(@"left");
}
if (direction & TTGDirectionRight) {
NSLog(@"right");
}
if (direction & TTGDirectionBottom) {
NSLog(@"bottom");
}
direction变量的实际内存如下:
0 0 0 0 1 0 1 1
控制台输出:
top
left
bottom
这样,用位运算,就可以同时支持多个值。
enum在 Objective-C 中的“升级版”
一般来说,我们不能指定枚举变量的实际类型是什么,就是说,我们不知道枚举最后是 int 型,还是其他的什么类型。但是从 C++ 11开始,我们可以为枚举指定其实际的存储类型,如下语法:
enum TTGState : NSInteger {/*...*/};
但是,我们在定义枚举的时候如何保证兼容性呢?Foundation 框架已经为我们提供了更加“统一、便捷”的枚举定义方法,如下:
//NS_ENUM,定义状态等普通枚举
typedef NS_ENUM(NSUInteger, TTGState) {
TTGStateOK = 0,
TTGStateError,
TTGStateUnknow
};
//NS_OPTIONS,定义选项
typedef NS_OPTIONS(NSUInteger, TTGDirection) {
TTGDirectionNone = 0,
TTGDirectionTop = 1 << 0,
TTGDirectionLeft = 1 << 1,
TTGDirectionRight = 1 << 2,
TTGDirectionBottom = 1 << 3
};
所以,在 iOS 开发中,枚举最好使用NS_ENUM
和NS_OPTIONS
定义,并指明其底层数据类型,保证统一。
处理枚举类型 switch 语句中不要实现 default 分支
这样的话,加入新枚举之后,编译器就会提示开发者 switch 语句并未处理所有枚举。如果写上了 default 分支,那么它就会处理这个新状态,从而导致编译器不发警告信息。
参考资料
1、Effective Objective-C 2.0
2、https://juejin.im/post/5aaf6943518825556e5de48e
3、http://www.cocoachina.com/ios/20161110/18035.html
4、https://www.cnblogs.com/feiyu-mdm/p/6392493.html