[编写高质量iOS代码的52个有效方法](一)Objective-C基础
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹为快
1.了解Objective-C语言的起源
2.在类的头文件中尽量少引入其他头文件
3.多用字面量语法,少用与之等价的方法
4.多用类型常量,少用#define预处理器指令
5.用枚举表示状态、选项、状态码
目录
- 编写高质量iOS代码的52个有效方法一Objective-C基础
- 先睹为快
- 目录
- 第1条了解Objective-C语言的起源
- 第2条在类的头文件中尽量少引入其他头文件
- 第3条多用字面量语法少用与之等价的方法
- 第4条多用类型常量少用define预处理器指令
- 第5条用枚举表示状态选项状态码
第1条:了解Objective-C语言的起源
Objective-C与C++,Java等面向对象语言的区别在于Objective-C使用“消息结构”,而不是“函数调用”。Objective-C语言是由消息型语言鼻祖Smalltalk演化而来。
消息与函数调用之间的语法区别:
// 消息(Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
// 函数调用(C++)
Object *obj = new Object;
obj->perform(parameter1,parameter2);
关键区别在于:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。Objective-C的重要工作都由运行时组件而非编译器完成。
Objective-C是C的超集,所以C语言中的所有功能在编写Objective-C代码时依然适用。理解C语言的内存模型尤为重要,这有助于理解Objective-C的内存模型及其引用计数机制的工作原理。Objective-C语言中的指针是用来指示对象的。想要声明一个变量,另其指代某个对象:
NSString *someString = @"the string";
NSString *anotherString = someString;
两个变量都是指向NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在堆中,而不是栈中。不在再栈上分配Objective-C对象。而两个变量所占内存都分配在栈上,且两块内存里的值一样,都是NSString对象的内存地址。
分配在堆中的内存必须直接管理,而分配在栈中用于保存变量的内存则会在其栈帧弹出时自动清理。
在Objective-C代码中,有时也会遇到定义里不含*的变量,它们可能会使用栈空间,例如:
CGRect frame;
CGRect是C结构体,整个系统框架都在使用这种结构体。如果改用Objective-C对象来做,需要额外开销,如分配及释放堆内存等。如果只需要保存int、float、double、char等非对象类型,通常使用CGRect这种结构体就可以了。
第2条:在类的头文件中尽量少引入其他头文件
与C和C++一样,Objective-C也使用头文件与实现文件来区隔代码。
创建一个EOCPerson类,并在类中用到另一个类EOCEmployer类的实例
// EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer;
@interface EOCPerson : NSObject
@property(nonatomic, copy) NSString *firstName;
@property(nonatomic, copy) NSString *lastName;
@property(nonatomic, strong) EOCEmployer *employer;
@end
// EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
// 实现方法
@end
在头文件中用@class EOCEmployer;
语句告诉编译器需要用到这个类,不需要知道该类的全部细节,再在需要知道其所有接口细节的实现文件中用#import "EOCEmployer.h"
引入EOCEmployer类。这种做法叫做向前声明。如果直接在头文件中引入EOCEmployer.h,则会一并引入EOCEmployer.h中的所有内容,此过程持续下去,则要引入许多根本用不到的内容,增加编译时间,还可能造成循环引用。
但有些时候无法使用向前声明,例如自定义的类继承自某个超类或遵从某个协议。
// EOCRectangle.h
#import "EOCShape.h"
#import "EOCDrawable.h"
@interface EOCRectangle : EOCShape <EOCDrawable>
@property(nonatomic, assign) float width;
@property(nonatomic, assign) float height;
@end
如果自定义的类继承于某个超类,则必须引入定义那个超类的头文件,如果要遵从某个协议,尽量把该类遵循某协议的声明移到分类(什么是分类?)中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
第3条:多用字面量语法,少用与之等价的方法
Foundation框架中的NSString、NSNumber、NSArray、NSDictionary这4个类的实例的声明,即可以用常见的alloc及init方法,也可以直接用字面量语法来声明:
// 将整数、浮点数封入到Objective-C对象中,可以用字面量,也可以调用NSNumber中的方法。
// 字面量
NSNumber *intNumber = @1;
// 等价方法
NSNumber *intNumber = [NSNumber numberWithInt:1];
// 字面量
NSNumber *doubleNumber = @3.14159
// 等价方法
NSNumber *doubleNumber = [NSNumber numberWithDouble:3.14159];
// 字面量语法也适用于表达式
int x = 5;
float y = 6.32f;
NSNumber *expressionNumber = @(x * y);
// 使用方法和字面量语法创建数组
NSArray *animals = [NSArray arrayWithObjects:@"cat",@"dog",@"mouse",nil];
NSArray *animals = @[@"cat",@"dog",@"mouse"];
// 使用方法和字面量语法获取下标对应的对象
NSString *dog = [animals objectAtIndex:1];
NSString *dog = animals[1];
使用字面量的好处:假如创建一个含有3个对象的数组,第二个对象为nil,其他两个对象都有效,如果用字面量语法创建,则在运行时会抛出异常,可以更快找到错误。而如果用arrayWithObjects:方法,则不会抛出异常,但创建的数组会只包含第一个对象,因为该方法会依次处理各个参数,直到发现nil为止。这样会导致错误不容易被发现。
// 使用方法和字面量语法创建字典
NSDictionary *personData = [NSDictionary dictionaryWithObjectAndKeys:@"Matt",@"firstName",@"Galloway",@"lastName",[NSNumber numberWithInt:28],@"age",nil];
NSDictionary *personData = @{@"firstName":@"Matt",@"lastName":@"Galloway",@"age":@28};
// 使用方法和字面量语法获取下标对应的对象
NSString *lastName = [personData objectForKey:@"lastName"];
NSString *lastName = personData[@"lastName"];
用字面量语法创建字典也有类似优点,有助于查错。
使用字面量语法创建出来的字符串、数组、字典对象都是不可变的。若想要可变版本的对象,需要复制一份。
NSMutableArray *mutable = [@[@1,@2,@3] mutableCopy];
这么做会多调用一个方法,而且还需要再创建一个对象,但使用字面量语法利大于弊。
第4条:多用类型常量,少用#define预处理器指令
编写代码时经常要定义常量,例如,要写一个UI视图类,此视图显示出来之后就播放动画,然后消失。如果想将播放动画的时间提取为常量,通常会这么写:
#define ANIMATION_DURATION 0.3
但是这样定义出来的常量没有类型信息,且预处理过程会把碰到的ANIMATION_DURATION一律替换成0.3,假设此指令声明在某个头文件中,那么所有引入这个头文件的代码都会进行替换。更好的方式是定义一个类型为NSTimeInterval的常量
// EOCAnimatedView.h
#import <UIKit/UIKit.h>
@interface EOCAnimatedView : UIView
@end
// EOCAnimatedView.m
#import "EOCAnimatedView.h"
// 声明定义常量
static const NSTimeInterval EOCAnimationDuration = 0.3;
@implementation EOCAnimatedView
@end
变量一定要同时用static于const来声明。const修饰符可以保护变量不被修改。static修饰符则意味着仅在定义此变量的编译单元中可见。如果不加static,在另一个编译单元也声明了同名变量就会报错。这样创建的常量是不公开的。
如果需要对外公开某个常量,就需要常量放在全局符号表中,以便可以在定义该常量的编译单元之外使用:
// EOCAnimatedView.h
#import <UIKit/UIKit.h>
// 声明常量
extern const NSTimeInterval EOCAnimationDuration;
@interface EOCAnimatedView : UIView
@end
// EOCAnimatedView.m
#import "EOCAnimatedView.h"
// 定义常量
const NSTimeInterval EOCAnimationDuration = 0.3;
@implementation EOCAnimatedView
@end
此类常量必须要定义,而且只能定义一次。因为要放到全局符号表里,所以命名常量时需谨慎,避免名称冲突。
第5条:用枚举表示状态、选项、状态码
枚举是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集:
// 套接字连接状态
enum EOCConnectionState{
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
编译器会为每个枚举值分配一个独有的编号,从0开始,每个枚举递增1。实现枚举所用的数据类型取决于编译器,不过其二进制位(bit)的个数必须能完全表示下枚举编号才行。
C++11标准扩充了枚举的特性,Objective-C也能得益于C++11标准。其中一项改动是:可以指明用何种底层数据类型来保存枚举类型的变量。这样做的好处是,可以向前声明枚举变量了。
// 指定底层数据类型
enum EOCConnectionState : NSInteger {/* . . . */};
// 向前声明枚举变量
enum EOCConnectionState : NSInteger;
// 手动指定枚举成员的值,接下来的枚举值都会在上一个的基础上自动递增1
enum EOCConnectionState{
EOCConnectionStateDisconnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
还有一种情况应该使用枚举类型,那就是定义选项的时候。若这些选项可以彼此组合,则更应如此:
// 设备支持方向
enum EOCPermittedDirection{
EOCPermittedDirectionUp = 1 << 0, // 0001 上
EOCPermittedDirectionDown = 1 << 1, // 0010 下
EOCPermittedDirectionLeft = 1 << 2, // 0100 左
EOCPermittedDirectionRight = 1 << 3, // 1000 右
};
// direction枚举值为0101,表示支持上和左两个方向
enum EOCPermittedDirection direction = EOCPermittedDirectionUp|EOCPermittedDirectionLeft;
使用宏创建枚举类型(NS_ENUM与NS_OPTIONS都是Foundation框架中定义的辅助宏)
// 普通枚举类型
typedef NS_ENUM(NSUInteger, EOCConnectionState){
EOCConnectionStateDisconnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
// 选项枚举类型
typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection){
EOCPermittedDirectionUp = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
};
在switch语句中使用枚举:
typedef NS_ENUM(NSUInteger, EOCConnectionState){
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
EOCConnectionState state = EOCConnectionStateConnected;
switch (state) {
case EOCConnectionStateDisconnected:
// Handle disconnected state
break;
case EOCConnectionStateConnecting:
// Handle connecting state
break;
case EOCConnectionStateConnected:
// Handle connected state
break;
}
在switch语句中使用枚举时,最好不要有default分支,如果枚举中加入了一个新状态,编译器会发出警告信息提示有状态未在switch语句中处理,假如写上了default分支,那么就会导致编译器不会发出警告信息。通常要确保switch语句能正确处理所有枚举值。