第一章 熟悉Objective-C

时间:2024-01-22 22:17:20

第一条:了解Objective—C语言的起源

Objective—C语言是由Smalltalk演化而来,后者是消息型语言的鼻祖,所以该语言使用的“消息结构”而非“函数调用”。

1.消息和函数调用之间的区别

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

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

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

采用消息结构的语言,总是在运行时才会查找所要执行的方法。

Objective-C的重要工作都由**“运行期组件”**(runtime component)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说,运行期组件中含有全部内存管理方法。这样的话,只需更新运行期组件,即可提升应用程序性能。而那种许多工作都在“编译期”(compile time)完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。

2.Objective- C的内存模型

NSString stackString;
//error:interface type cannot be statically allocated

NSString *someString = @"The string";

因为OC声明变量基本上都为指针变量,所以OC对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。对象分配在栈上,而实例分配在堆中。

Objective-C将堆内存管理抽象出来了不需要用malloc及free来分配或释放对象所占内存。Objective-C 运行期环境把这部分工作抽象为一套内存管理架构,名叫**“引用计数”**
3.要点:

Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。

理解C语言的核心概念有助于写好Objective-C程序。尤其是要掌握内存模型和指针

第二条:在类的头文件中尽量少引入其他的头文件

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

//EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@end

//EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
//Implementation of methods
@end

1.在一个文件中引入另一个文件(向前声明):

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

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

常见的办法是在EOCPerson.h中加入下面这行:

#import"EOCEmployer.h"

这种办法可行,但是不够优雅。在编译一个使用了EOCPerson类的文件时,不需要知道EOCEmployer类的全部细节,只需要知道有一个类名叫EOCEmployer就好。所幸有个办法能把这一情况告诉编译器:

@class BOCEmployer;

这叫做“向前声明”(forward declaring)该类。

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

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

@implementation EOCPerson
// Implementation of methods
@end

2.向前声明的好处:

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

例如:有两个类,它们都在头文件中引入了对方的头文件,两个类都进行各自的引用解析,这样就会导致“循环引用”(chicken-and-egg situation)。
虽然我们使用#import而非#include不会导致死循环,但是这意味着两个类中有一个类无法被正确编译。

但是,有时候就必须引入头文件,比如继承以及遵循的协议,因为编译器要知道该协议中定义的方法。

//EOCRectangle.h
#import"EOCShape.h"
#import"EOCDrawable.h"
@interface EOCRectangle : EOCShape<EOCDrawable>
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
 end

3.要点:

除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。

有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循的协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

第3条,多用字面语法,少用与之等价的方法

Objective-C以语法繁杂而著称。不过,从Objective-C 1.0起,有一种非常简单的方式能创建NSString对象。这就是“字符串字面量”(string literal),其语法如下:

NsString *someString=@"Effective Objective-C 2.0";

如果不用这种语法的话,就要以常见的alloc及init方法来分配并初始化NSString对象了。在版本较新的编译器中,也能用这种字面量语法来声明 NSNumber、NSArray、NSDictionary类的实例。
1.字面数值
有时需要把整数、浮点数、布尔值封入Objective-C对象中。这种情况下可以用NSNumber类,该类可处理多种类型的数值。若是不用字面量,那么就需要按下述方式创建实例:

NSNumber *someNumber =[NSNumber numberWithInt:1];

然而使用字面量能令代码更为整洁:

NSNumber *someNumber= @1;

能够以NSNumber实例表示的所有数据类型都可使用该语法。例如:

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
//对运算也适用
int x = 5;
float y = 6.32f;
NSNumber *expressionNumber = @(x * y);

2.字面量数组:

之前创建一个数组:

NSArray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];

而使用字面量语法来创建:

NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];

上面的创建方法不仅简单而且还利于操作数组,就比如访问数组的元素,之前是:

NSString *dog = [animals objectAtIndex:1];

使用字面量就可以直接:

NSString *dog = animals[1];

下面这段代码分别以两种语法创建数组:

id objectl=/* ...*/;
id object2=/*...*/;
id object3=/*...*/;
NSArray *arrayA =[NSArray array Withobjects:
object1, object2, object3, ni1];
NSArray *arrayB =[object1, object2, object3];

如果object1与object3都指向了有效的Objective-C对象,而object2是nil,那么会出现什么情况呢?
按字面量语法创建数组arrayB时会抛出异常。arrayA虽然能创建出来,但是其中却只含有objectl一个对象。原因在于,“arrayWithObjects:”方法会依次处理各个参数,直到发现nil为止,由于object2是nil,所以该方法会提前结束。

3.字面量字典

官方初始化字典变量,两两一对,顺序为<对象>,<键>:

NSDictionary *personData = [NSDictionary dictionaryWithObjectivesAndKeys:@"Mett", @"firstName", @"Galloway", @"lastName", [NSNumber numberWithInt:28], @"age", nil];

这样写与我们通常理解的模式不太相同,理解起来可能会有点麻烦,顺序改为<键>,<对象>,所以我们可以使用字面量定义:

NSDictionary *personData = @{@"firstName": @"Matt", @"lastName": @"Galloway", @"age": @28};

这样写我们理解起来就简单的多了,并且这个与数组相同,只要遇到nil就会抛出异常,这有助于查错。

当然字典变量的访问也可以使用字面量方法:

NSString *lastName = personData[@“lastName”];

4.可变数组与字典:

通过取下标操作,可以访问数组中的某个元素或者字典中的某个键对应的元素,如果数组和字典是可变的(mutable),那么也能通过下标修改其中的元素值,标准做法

[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"Galloway"forKey:@"lastName"];

用下标操作:

mutableArray[1] = @"dog";
mutableDictionary[@"lastName"] = @"Galloway";

5.局限性
使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,则需复制一份:

NSMutableArray *mutable=[@ [@1, @2, @3, 4, @5]mutableCopy];

这么做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多于上述缺点的。

6.要点:

应该使用字面量语法来创建字符串、数值、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。

应该通过取下标操作来访问数组下标或者字典中的键对应的元素。

用字面量语法创建数组或者字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。

第4条:多用类型常量,少用#define预处理指令

通常我们在写程序的时候都会使用#define来定义一个固定的数据,方便我们后续自己的编写,但是这样定义出来的常量没有类型信息,并且假设此命令在某个头文件中,那么所有引入了这个头文件的的代码,其定义的固定值都会被这个替换掉,反而破坏了程序。

那么这个时候我们就可以使用下面的方法:

static const NSTimeInterval kAnimationDuration = 0.3;

这种方式定义的常量包含类型信息,其好处是清楚的描述了常量的含义。由此可知,该常量类型为NSTimeInterval,这有助于其编写开发文档。

1.常量常用的命名法:

若常量局限于某“编译单元”(也就是“实现文件”)之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。

2.常量的定义位置:

我们最好不要将常量定义在头文件中,若你定义在头文件中,又被其他的文件引用了,那么该这个文件中的这个常量都会被其替换掉,所以最好不要在头文件中定义常量,不论你是如何定义常量的,因为OC中没有“名称空间”这一概念。

变量一定要同时用static与const来声明。如果试图修改由const修饰符所声明的变量,那么编译器就会报错。而static修饰符则意味着该变量仅在定义此变量的编译单元中可见。在Objective-C的语境下,“编译单元”一词通常指每个类的实现文件(以.m为后缀名)。
假如声明此变量时不加static,则编译器会为它创建一个“外部符号”(external symbol)。此时若是另一个编译单元中也声明了同名变量,那么编译器就抛出一条错误消息:

有时候我们需要对外公开我们的常量,比如说是通知时的通知名称,我们定义一个常量,外界就可以直接使用这个常值变量来注册自己想要接收的通知即可,而不用知道实际字符串的值。
此类常量需放在“全局符号表”中,以便可以在定义该常量的编译单元之外使用。举例说明:

//In the header file
extern NSString *const EOCStringConstant;

//In the implementation file
NSString *const EOCStringConstant = @"VALUE";

这个常量在头文件中“声明”,且在实现文件中“定义”。注意const修饰符在常量类型中的位置。常量定义应从右至左解读,所以在本例中,EOCStringConstant就是“一个常量,而这个常量是指针,指向NSString对象”。这与需求相符:我们不希望有人改变此指针常量,使其指向另一个NSString对象。

extern就是告诉编译器,在全局符号表中将会有一个名叫EOCStringConstant的符号,也就是说,编译器无需查看其定义,即允许代码使用此常量。

3.要点:

不能用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。

在实现文件中使用static const来定义“只在编译单元内可见的常量”。由于此常量不在全局符号表中,所以无需为其名称加前缀。

在头文件中使用extern来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应该加以区隔,通常用与之相关的类名做前缀。

第5条:用枚举表示状态、选项、状态码

要点:

应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。

如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。

用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。

在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。