读Effective Objective-C 2.0(一)
第1条:了解Objective-C语言的起源
- Objective-C语言使用消息结构(messaging structure)由 Smalltalk演化而来。
- 使用消息结构的语言,其运行时所应执行的代码由运行环境来决定,而使用函数调用的语言(C++),则由编译器决定。
objective-c copyable">NSString *someString = @"The string"; // 声明一个变量,指向某个对象
- Objective-C语言中的指针是用来指示对象的,对象所占内存总是分配在堆空间(heap space)。
NSString *someString = @"The string";NSString *anotherString = someString;
- someString 变量指向分配在堆里的某块内存(someSting存储着是该块堆内存的地址,即NSString实例的地址),如果再创建一个变量,另起指向同一地址,那么并不拷贝该对象,只是这两个变量同时指向该对象。
- Objective-C是C语言的超集(superset),Objective-C使用动态绑定的消息结构,在运行时检查对象类型。接收到1条消息后,究竟应该执行何种代码,由运行期环境决定,而非编译器决定。(即Objective-C是动态的语言)
第2条:在类的头文件中尽量少引入其他头文件
2.1 向前声明提高编译速度
- 定义一个Person类,创建类成功会自动引入
Foundation.h
,相当于引入了整个Foundation框架
,因为今后在使用Person类的时候必定会用到Foundation框架
的许多内容。所以引入整个Foundation框架
没有任何问题。
// Person.h#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
@implementation Person
@end
- 过段时间,可能我们需要创建一个Employer的新类,而且要求每个Person实例中都包含一个Employer,于是我们需要给Person添加一个属性
- 添加属性后,编译器就会报错,未知的Employer类型,为了能找到Employer类型,必须要引入Employer.h
- 常见的办法是
#import "Employer.h"
。但是这种办法不够优雅。因为在使用Person类的时候,并不需要知道Employer类的全部细节,只需要知道一个类名叫Employer即可。
// Person.h#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, span) Employer *employer;
@end
NS_ASSUME_NONNULL_END
- 基于上面的问题,有另外一个办法,叫做向前声明(forward declaring)即使用
@class 类名;
在.h文件声明一下,告诉编译器有这么一个类。
#import <Foundation/Foundation.h>@class Employer;
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, span) Employer *employer;
@end
NS_ASSUME_NONNULL_END
如果Person类的实现文件,真正的需要使用到Employer类的时候,就要知道其所有接口的细节,所以在Person类的实现文件中来
import
Employer就可以了。将引入头文件的时机尽量延后,只在真正用到的时候再引入,这样可以减少类的使用者所需引入的头文件数量,可以提高编译速度。
2.2 向前声明解决了2个类相互引用的问题
- 假设要为Employer类加入新增和删除雇员的方法,头文件会定义如下方法
- (void)addEmployee:(Person *)person;- (void)removeEmployee:(Person *)person;
- 此时,编译器想要编译Employer,编译器必须知道Person这个类,而要编译Person类,又必须知道Employer。如果在各自的头文件引入对方的头文件,则会导致循环引用(chicken-and-egg situation)。当解析其中一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又引用第一个头文件。编译肯定不会通过。此时使用向前声明即可解决该问题。
2.3 什么时候不能使用@class向前声明
如果你写的类继承自某个类,则必须引入定义那个类的头文件
如果你写的类遵守某个协议,那么该协议必须有完整定义,且不能使用向前声明,向前声明只能告诉编译器有某个协议,而此时编译器需要知道该协议中定义的方法
假设Employer类需要遵守一个叫做Runable的协议,实现其中的run方法,那么就必须使用
import
引入协议的头文件,进而实现协议中的方法。鉴于此,我们最好是把协议单独定义在一个文件中。如果我们把Runable定义在Person类中,假设Person类非常的庞大,我们将会引入大量用不到的接口,反而增加了编译的速度,还会产生相互依赖的问题。
第3条:多用字面量语法,少用与之等价的方法
NSString
,NSNumber
,NSArray
,NSDictionary
这几个类,都可以使用字面量语法,使用字面量语法可以缩减源代码的长度,提高代码的可读性
3.1 NSString
NSString *someString = @"Effective Objective-C 2.0";
3.2 NSNumber
可以将整数、浮点数、布尔值封装成NSNumber类型
不使用字面量创建一个数字
NSNumber *someNumber = [NSNumber numberWithInt:1];
- 使用字面量创建一个数字
NSNumber *someNumber = @1;
使用字面量语法,代码更加精简和直观
能够以NSNumber实例表示的所有数据类型都可以使用该语法
NSNumber *intNumber = @1;NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.1415926;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
- 字面量语法也适用于表达式
int x = 3;float y = 0.14159;
NSNumber *expressionNumber = @(x + y);
3.3 NSArray
- 不使用字面量语法创建数组
NSArray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];
- 使用字面量语法创建数组
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
- 不使用字面量获取某个下标所对应的对象
NSString *dog = [animals objectAtIndex:1];
- 使用字面量获取某个下标所对应的对象
NSString *dog = animals[1];
- 使用字面量语法创建数组,若数组对象中有nil,则会抛出异常
NSObject *obj1 = [[NSObject alloc] init];NSObject *obj2 = nil;
NSObject *obj3 = [[NSObject alloc] init];
NSArray *objs = @[obj1, obj2, obj3];
// 异常信息:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]'
- 若使用
arrayWithObjects:
创建的数组,若数组中有nil,不会抛出异常 - 原因在于
arrayWithObjects
方法会依次处理各个参数,知道发现nil为止,由于object2是nil,所以该方法会提前结束,数组中只有obj1一个对象。
NSObject *obj1 = [[NSObject alloc] init];NSObject *obj2 = nil;
NSObject *obj3 = [[NSObject alloc] init];
NSArray *objs = [NSArray arrayWithObjects:obj1, obj2, obj3, nil];
- 这种差别表明,使用字面量语法更加安全,抛出异常可以快速发现错误,比创建好数组之后才发现元素的个数少了要好。
3.4 NSDictionay
- 字典是一种映射结构,可向其中添加键值对。
- 不使用字面量语法创建字典
- 这种创建方式与通常理解的方式顺序相反,通常理解是key-value,而这种创建方式是value-key
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"Lebron", @"firstName",
@"James", @"lastName",
[NSNumber numberWithInt:36], @"age",
nil];
- 使用字面量语法创建字典
- 这种写法可读性肯定更好,代码也更加简洁
NSDictionary *dict = @{@"firstName" : @"Lebron",
@"lastName" : @"James",
@"age" : @36
};
同样的使用字面量创建字典,一旦有值为nil,就会抛出异常
不使用字面量语法根据键进行取值
NSString *firstName = [dict objectForKey:@"firstName"];
- 使用字面量语法根据键进行取值
NSString *firstName = dict[@"firstName"];
第4条:多用类型常量,少用#define预处理指令
编写代码时经常要定义常量。定义常量的目的是将常量值进行抽取,相同的常量值只需要修改定义常量的地方即可,可以减少代码的维护成本。
比如:定义动画的时长,自定义通知的key值,固定宽高的控件等。Objective-C中可以定义使用#define
定义预处理指令实现这个功能。
#define ANIMATION_DURATION 0.3
这个预处理指令会把源代码中所有的ANIMATION_DURATION
替换为0.3,虽然实现了我们想要的效果,但是这样定义出来的常量没有类型信息。ANIMATION_DURATION
是与持续时间有关的常量,但是从这个预处理指令中是没有明确指出的。
针对这个问题,引出了另外一种定义常量的方式:类型常量
static const NSTimeInterval kAnimationDuration = 0.3;
这种定义常量的方式,就包含了类型信息,清楚地描述了常量的含义。
类型常量的命名规则:
- 若常量局限于某一个实现文件里。则在前面加上字母k。
- 若常量在类之外可见,则通常以类名为前缀。
如果不打算公开某个常量,应该将其定义在使用该常量的实现文件里。不要定义在头文件中,如果定义在某个头文件中,当头文件被很多地方引用,那么属于局部的常量就会被公开。而其实我们是不希望这个常量被公开使用的。
定义类型常量,使用两个关键字static
和 const
。使用const
关键字修饰的变量,是不允许修改的,一旦修改编译器就会报错。而使用static
修饰符代表该变量只能在定义此变量的实现文件(.m)中可见。
如果声明此变量时不加static
关键字,则编译器会为它创建一个外部符号(external symbol),此时如果另一个实现文件也声明了同名的变量,那么编译器就会报错:
duplicate symbol '_kAnimationDuration'in:/Users/bh/Library/Developer/Xcode/DerivedData/test-dmgddqbpgsjlpvgeridhhmhanpfe/Build/Intermediates.noindex/test.build/Debug-iphonesimulator/test.build/Objects-normal/x86_64/Car.o
/Users/bh/Library/Developer/Xcode/DerivedData/test-dmgddqbpgsjlpvgeridhhmhanpfe/Build/Intermediates.noindex/test.build/Debug-iphonesimulator/test.build/Objects-normal/x86_64/Ship.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
如果一个变量既声明为static
又声明为const
,那么编译器就不会创建符号了。
上面的情况是定义常量只想用在局部的类里,但有些时候我们确实需要公开某个常量。比如说使用NSNotificationCenter的时候,需要多个类都使用字符串常量命名Key值。
此类常量要放在全局符号表(global symbol table)中。
通常情况下,我们可以定义一个Objective-C文件,包含xxxConst.h 和xxxConst.m。
// In the header fileextern NSString *const = EOCStringConstant;
// In the implementation file
NSString *const EOCStringConstant = @"VALUE";
这个常量在头文件中声明,在实现文件中定义。当编译器发现使用了extern
关键字,就能明白如何在引入头文件的代码中处理该常量了。extern
关键字告诉编译器,在全局符号表中有一个叫做EOCStringConstant的符号。当链接成二进制文件后,肯定能找到这个常量。
编译器会在数据段(data section)为字符串分配空间。起名字的时候要注意,为了避免命名冲突,最好是用与之相关的类名做前缀。可以参照系统的命名方式如UIApplicationDidEnterBackgroundNotification
等。
第5条:用枚举表示状态、选项、状态码
枚举类型以一系列常量来表示状态码或者可组合的选项,在系统框架中被频繁使用。
某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)。比如,用枚举表示套接字连接(socket connection)的各种状态:
enum EOCConnectionState {EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
每种状态都用一个便于理解的值来表示,提高了代码的可读性。编译器会为枚举分配一个独有的编号,从0开始,每个枚举递增1。实现枚举所用的数据类型取决于编译器。
定义一个枚举:
enum EOCConnectionState state = EOCConnectionStateDisconnected;
这样定义枚举每次都需要写上enum
关键字,不是很简洁,所以可以使用typedef
关键字重新定义枚举类型。
enum EOCConnectionState {EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;
再定义一个枚举:省略了enum
关键字,更加简洁。
EOCConnectionState state = EOCConnectionStateConnected;
- 定义枚举的时候,可以指定底层数据类型(underlying type)来保存枚举类型的变量,这么做的好处是可以提前告诉编译器为枚举变量分配多少内存空间
- 还可以不使用编译器分配的序号,而是手动指定某个枚举成员所对应的值。
enum EOCConnectionState : NSInteger {EOCConnectionStateDisconnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
指定EOCConnectionStateDisconnected
的值为1,那么接下来几个枚举值会在上一个基础加1。
在定义选项的时候,也应该使用枚举类型。尤其是这些选项可以彼此组合的时候。各个选择之间可通过按位或操作来进行组合。
比如:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
因为每个枚举值所对应的二进制表示中,只有1个二进制位是1。使用按位或操作符可以组合多个选项了。
比如:
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeightUIViewAutoresizingFlexibleWidth 二进制表示 0 0 0 0 1 0 // 左移1位
|
UIViewAutoresizingFlexibleWidth 二进制表示 0 1 0 0 0 0 // 左移4位
------------------------------------------------------------------
0 1 0 0 1 0
取到按位或之后得到二进制的值,如果想要判断使用了哪个枚举,只需要拿到按位或的结果 和 某一个枚举值按位与就可以了。
举个例子:
// 0 1 0 0 1 0enum UIViewAutoresizing resizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (resizing & UIViewAutoresizingFlexibleWidth) {
// 设置了UIViewAutoresizingFlexibleWidth
// 0 1 0 0 1 0
// &
// 0 0 0 0 1 0
----------------
0 0 0 0 1 0
}
Foundation框架中定义了一些辅助的宏,用这些宏定义枚举类型时,也可以指定用于保存枚举值得底层数据类型。这些宏具备向后兼容能力,如果目标平台的编译器支持新标准,那就使用新式语法,否则使用旧式语法。
这些宏使用#define 预处理指令定义的,一个用来定义普通的表示状态的枚举类型,另一个用来定义组合的枚举类型。
typedef NS_ENUM(NSUInteger, EOCConnectionState) {EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
}
typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection) {
EOCPermittedDirectionUp = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
}
凡是需要以按位或操作来组合的枚举都应该使用NS_OPTIONS
定义。如果枚举不需要组合,则应使用NS_ENUM
来定义。
- 开发建议:如果用枚举来定义状态,使用switch语句的时候,建议不要有default语句,不加default语句的话,如果有的状态没有在switch语句中列举,编译器会报警告,以防止程序员遗漏某个状态的处理。如果加上default语句,不会有该警告。
以上是 读Effective Objective-C 2.0(一) 的全部内容, 来源链接: utcz.com/a/34716.html