读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类的实现文件中来importEmployer就可以了。

  • 将引入头文件的时机尽量延后,只在真正用到的时候再引入,这样可以减少类的使用者所需引入的头文件数量,可以提高编译速度。

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条:多用字面量语法,少用与之等价的方法

  • NSStringNSNumberNSArrayNSDictionary这几个类,都可以使用字面量语法,使用字面量语法可以缩减源代码的长度,提高代码的可读性

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。
  • 若常量在类之外可见,则通常以类名为前缀。

如果不打算公开某个常量,应该将其定义在使用该常量的实现文件里。不要定义在头文件中,如果定义在某个头文件中,当头文件被很多地方引用,那么属于局部的常量就会被公开。而其实我们是不希望这个常量被公开使用的。

定义类型常量,使用两个关键字staticconst。使用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 file

extern 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 | UIViewAutoresizingFlexibleHeight

UIViewAutoresizingFlexibleWidth 二进制表示 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 0

enum 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

回到顶部