[Effective Objective] 熟悉Objective-C

了解 Objective-C

Objective_C 是一种面向对象的语言。但与jave、C++等语言不同,它使用了消息结构(messaging structure)而非函数调用(function calling)。Objective-C由Smalltalk演化而来,后者是消息语言的鼻祖。

消息与函数调用的区别看上去就像这样:

// Messaging (Objective-C)
Object* obj = [Object new];
[obj performWith:parameterl and:parameter2];

// Function calling (C++)
Object* obj = new Object;
obj->perform(parameter1, parameter2);

关键区别在于:使用消息结构的语言,其运行时所应执行的代码有运行环境来决定;而使用函数调用的语言,则由编译器决定。

运行期组件(runtime component)

Objective-C的重要工作都由运行期组件(runtime component)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。

运行期组件本质上就是一种与开发者所编代码相链接的“动态库”(dynamic library),其代码能把开发者编写的所有程序粘合起来。这样只需更新运行期组件,即可提升应用程序性能。

Objective-C内存模型

若要理解内存模型,则需明白:Objectivec-C语言中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用以下语法:

NSString *someString = @"The string";

它声明了一个名为someString的变量,其类型是NSString*。即此变量是指向NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。

someString变量指向分配在堆里的某块内存,其中含有一个NSString对象。也就是说,如果再创建一个变量,令其指向同一地址,那么并不拷贝该对象,只是这两个对象会同时指向此对象:

NSString *someString = @"The string";
NSString *anotherString = someString;

此时由两个NSString*型变量指向一个NSString实例。如下图:
在这里插入图片描述

分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈桢弹出时自动清理。

Objective-C运行期环境吧堆内存管理工作抽象为一套内存管理结构,名叫“引用计数”。

结构体

与创建对象相比,创建结构体可以减少许多开销,例如分配及释放堆内存等。如果只需保存int、float、double、char等“非对象类型”,那么通常使用结构体就可以了。

例如CGRect,其定义是:

struct CGRect {
	CGPoint origin;
	CGSize size;
};
typedef struct CGRect  CGRect;

要点

  • Objective-C为C语言添加了面向对象特性,是其超急。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
  • 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型与指针。

在类的头文件中尽量少引入其他头文件

与C和C++一样,Objective-C也使用“头文件”(header file)与“实现文件”(implementation file)来区隔代码。用Objective-C语言编写“类”的标准方式为:以类名做文件名,分别创建两个文件,头文件后缀用.h,实现文件后缀用.m。创建好一个类之后,其代码看上去如下所示:

// 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>

@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end

此时比起在EOCPerson.h下加入下面这行:

#import "EOCEmployer.h"

更推荐使用下面方法:

@class EOCEmployer;

这个方法叫做“向前声明”(forward declaring)该类。现在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类的全部细节,只需要知道有一个类名叫EOCEmploer就好。

EOCPerson类的实现文件则需引入EOCEmployer类的头文件,因为若要使用后者,则必须知道其所有接口细节。于是,实现文件就是:

//EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"

@implementation EOCPerson
// Implementation of methods
@end

尽量将引入头文件的时机延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入头文件数量,减少编译时间。

相互引用

向前声明也解决了两个类相互引用的问题。

当编译器解析一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件,就会导致“循环引用”(chicken-and-egg situation)。使用#import而非#include指令虽然不会导致死循环,但这却意味着两个类里有一个无法被正确编译。

必须引入头文件

但是有时候必须要在头文件中引入其他头文件。如果你写的类继承自某个超类,则必须引入定义那个超类的头文件。同理,如果要声明你写的类尊从某个协议(protocol),那么该协议必须有完整定义,且不能使用向前声明。

例如,要从图形类中继承一个矩形类,且令其遵循绘制协议:

// EOCRectangle.h

#import "EOCShape.h"
#import "EOCDrawable.h"

@interface EOCRectangle : EOCShape<EOCDrawable>
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
@end

此时第二条#import是难免的。鉴于此,最好是把协议单独放在一个头文件中,以避免相互依赖问题。

然而有些协议,例如“委托协议”,就不用单独写一个头文件了。在那种情况下,协议之一与接收协议委托的类放在一起定义才有意义。此时,我们可以把在实现文件中声明此类实现了该委托协议,并把实现代码放在“class-continuation 分类”里。这样的话,只要在实现文件中引入包含委托协议的头文件即可,而不需要将其放在公共头文件里。

要点

  • 厨房确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
  • 有时无法使用向前声明,比如要声明某个类遵循某一项协议。这种情况下,尽量把“该类遵循某协议”的声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

多用字面量语法,少用与之等价的方法

字符串字面量

不使用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。

多用类型常量,少用#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来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。

用枚举表示状态、选项、状态码

在以一系列常量来表示错误状态码或可组合的选项时,极宜使用枚举为其命名。

枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)。比如说,下例枚举表示“套接字连接”的状态:

enum EOCConnectionState {
    EOCConnectionStateDisconnected,
    EOCConnectionStateConnecting,
    EOCConnectionStateConnected,
};

由于每种状态都用一个便于理解的值来表示,所以这样写出来的代码更易读懂。

还可以使用typedef关键字重新定义枚举类型:

enum EOCConnectionState {
    EOCConnectionStateDisconnected,
    EOCConnectionStateConnecting,
    EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;

现在可以用简写的EOCConnectionState来代替enum EOCConnectionState了:

EOCConnectionState state = EOCConnectionStateDisconnected;

向前声明枚举类型

C++11标准修订了枚举的某些特性。其中一项改动是:可以指明用何种“底层数据类型”来保存枚举类型的变量。这样做的好处是,可以向前声明枚举变量了。

指定底层数据类型所用的语法是:

enum EOCConnectionStateConnectionState : NSInteger {/*...*/};

上面这行代码确保枚举的底层数据类型是NSInteger。也可以在向前声明时指定底层数据类型:

enum EOCConnectionStateConnectionState : NSInteger;

还可以不使用编译器所分配的序号,而是手工指定某个枚举成员所对应的值。语法如下:

enum EOCConnectionState {
    EOCConnectionStateDisconnected = 1,
    EOCConnectionStateConnecting,
    EOCConnectionStateConnected,
};

使用枚举类型定义选项

只要枚举定义的对,各选项之间就可通过“按位或操作符”(bitwise OR operator)来组合。例如,iOS UI框架中有如下枚举类型,用来表示某个视图应该如何在水平或垂直方向上调整大小:

enum UIViewAutoresizing {
	UIViewAutoresizingNone                      = 0,
	UIViewAutoresizingFlexibleLeftMargin        = 1 << 0,
	UIViewAutoresizingFlexibleWidth             = 1 << 1,
	UIViewAutoresizingFlexibleRightMargin       = 1 << 2,
	UIViewAutoresizingFlexibleTopMargin         = 1 << 3,
	UIViewAutoresizingFlexibleHeight            = 1 << 4,
	UIViewAutoresizingFlexibleBottomMargin      = 1 << 5,
}

每个选项均可启用或禁用,使用上述方式来定义枚举值即可保证这一点,因为每个枚举值所对应的二进制表示中,只要1个二进制位的值是1。用“按位或操作符”可组合多个选项。如下图:
在这里插入图片描述

宏定义枚举类型

NS_ENUM

用法:

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
	EOCConnectionStateDisconnected,
	EOCConnectionStateConnecting,
	EOCConnectionStateConnected,
};

如果支持新特性,那么用NS_ENUM所定义的枚举类型展开之后就是:

typedef enum EOCConnectionState : NSUInteger EOCConnectionState;
enum EOCConnectionState : NSUInteger {
	EOCConnectionStateDisconnected,
	EOCConnectionStateConnecting,
	EOCConnectionStateConnected,
};

NS_OPTIONS

用法:

NS_OPTIONS(NSUInteger, EOCPermittedDirection) {
	EOCPermittedDirectionUp     = 1 << 0,
	EOCPermittedDirectionDown   = 1 << 1,
	EOCPermittedDirectionLeft   = 1 << 2,
	EOCPermittedDirectionRight  = 1 << 3,
};

此时,根据是否将代码按C++模式编译,NS_OPTIONS宏的定义方式也有所不同。原因在于,在用或运算操作两个枚举值时,C++认为运算结果的数据类型应该是枚举的底层数据类型,也就是NSUInterger。而且C++不允许将这个底层类型“隐式转换”为枚举类型本身。所以C++模式下应该用另一种方式定义NS_OPTIONS宏,以便省去类型转换操作。

NS_NUME 与NS_OPTIONS宏定义

在这里插入图片描述

由于需要分别处理不同情况,所以上述代码用多种方法来定义这两个宏。第一个#if用于判断编译器是否支持新式枚举。其中所用的布尔逻辑看上去相当复杂,不过其意思就是想判断编译器是否支持新的枚举特性。如果不支持,那么就用老式语法来定义枚举。

NS_NUME 与NS_OPTIONS的选择

凡是需要以按位或操作来组合的枚举都应使用NS_OPTIONS定义。若是枚举不需要互相组合,则应使用NS_ENUM来定义。

switch语句处理枚举

有时可以这样定义:

typedef NS_ENUM(NSUInteger, EOCConnectState) {
	EOCConnectStateDisconnected,
	EOCConnectStateConnecting,
	EOCConnectStateConnected,
};

switch (_currentState) {
	EOCConnectStateDisconnected:
		// Handle disconnected state
		break;
	EOCConnectStateConnecting:
		// Handle connecting state
		break;
	EOCConnectStateConnected:
		// Handle connected state
		break;
}

若是用枚举来定义状态来定义状态机(state machine),则最好不要有default分支。这样的话,如果稍后又加了一种状态,那么编译器就会发出警告信息,提醒新加入的状态并未在switch分支中处理。

要点

  • 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个简单易懂的名字。
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
  • 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器多选的类型。
  • 在处理枚举类型的switch语句中不要实现default分支。这样的话加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值