文章目录
前言
最近开始阅读一些iOS开发的相关书籍,第一本就是《Effective Objective-C 2.0》,这里对第一周的阅读内容进行简单归纳和总结,主要是熟悉OC语言。
了解OC语言的起源
OC语言由Smalltalk演化而来,其使用“消息结构”而非“函数调用”。
对于消息和函数调用的区别,可以通过如下代码来体现:
//Messaging(OC)——"消息结构"
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
//Function calling(C++)——“函数调用“
Object *obj = new Object;
obj->perform(parameter1, parameter2);
消息结构和函数调用的关键差别在于:使用消息结构的语言,其运行所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。
OC的重要工作都由“运行期组建”而非编译器来完成。OC是C的“超集”,所以C语言中的所有功能在编写OC时仍然适用 。
OC语言中的指针是用来指示对象的。想要声明一个变量。令其指代某个对象,可用如下语法:
NSString *someString = @"The string";
上述代码声明了一个名为someString的变量,其类型为NSString*,即此变量为指向NSString的指针。所有的OC语言的对象声明必须以指针的形式,因为对象所占内存总是分配在“堆空间(heap space)”中,而绝不会在“栈(stack)”上。
someString变量指向分配在堆里的某块内存,其中含有一个NSString对象,若再创建一个变量,令其指向同一地址,如下:
NSString *someString = @"The string";
NSString *anotherString = someString;
则并不会拷贝该对象,只是两个变量同时指向一个对象。即只有一个NSString实例,但有两个变量指向此实例,两个变量都是NSString类型。如图:
但是,在OC中有时会遇到定义里不带有的变量,它们可能使用到“栈空间”,这些变量保存的不是CO对象。例如:
CGRect frame;
frame.origin.x = 0.0f;
frame.origin.y = 10.0f;
frame.size.width = 100.0f;
frame.size.height = 150.0f;.
小结
- OC为C语言添加了面向对象特征,是其的超集。OC使用动态绑定的消息结构,即在运行时才会检查对象类型。接受一条消息后,究竟应执行什么代码,有运行环境而非编译器来决定。
- 理解C语言的核心概念有助于写好OC程序。尤其要掌握内存模型和指针。
在类的头文件中尽量少引入其他头文件
假设我们有两个类。Person类可能会拥有一些Book类的对象,作为他所拥有的藏书。
Book.h头文件:
#import <Foundation/Foundation.h>
@interface Book : NSObject
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *author;
@end
在Person.h头文件中,假设我们只是想声明一个Book的指针作为成员变量(表示这个人拥有的一本书),我们如果如下在Person.h文件中直接引入Book.h文件,就在两者之间建立了一种依赖关系。
如果过多地引入头文件,会导致代码的耦合性增加,还可能引入许多根本用不到的内容,会增加代码的编译时间和维护成本。
Person.h头文件:
#import <Foundation/Foundation.h>
#import "Book.h"
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) Book *favoriteBook;
@end
为了避免上述情况,在Person.h头文件中,如果我们只是想声明一个Book的指针作为成员变量(表示这个人拥有的一本书),我们可以使用向前声明来避免引入Book.h头文件,如下:
Person.h头文件:
#import <Foundation/Foundation.h>
@class Book;
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) Book *favoriteBook;
@end
然后在Person.m实现文件中,当我们需要真正使用Book类的完整定义(例如访问Book的属性或者调用Book的方法)时,再引入Book.h头文件:
Person.m文件:
#import "Person.h"
#import "Book.h"
@implementation Person
// 这里可以使用Book类的完整定义来实现Person类的方法
// 比如设置favoriteBook的属性等操作
@end
向前引用:
- 解决了头文件循环引用的问题
- 但当涉及协议(Protocol)相关操作时,进行向前声明就无法满足需求了。在 Objective - C 中,当一个类声明遵循某个协议(Protocol)时,编译器需要知道协议的完整定义来检查该类是否正确地实现了协议中的方法。这时就必须要引入头文件,而不能使用向前声明了。
小结
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类(使用@class),并在实现文件中引入那些类的头文件(使用import)。这样做可以尽量降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至分类中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
多用字面量语法,少用与之等价的方法
//字面量语法
NSString *someString = @"Effective Objective-C";
//与之等价的创建方法
NSString *someString = [[NSString alloc] initWithString:@"Effective Objective-C"];
字面量数值
//字面数值
NSNumber *someNumber = @1;
//与之等价的数值创建方法
NSNumber *someNumber = [NSNumber numberWithInt:1];
能够以NSNumber实例表示的所有数据都可以使用该语法:
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.14159;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
//字面量语法对于以下表达式也适用
int x = 5;
float y = 6.32f;
NSNumber *expressionnumber = @(x * y);
字面量数组
//字面量语法
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
NSString *dog = animals[1];
//与之等价的方法
NSArray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];
NSString *dog = [animals objectAtIndex:1];
但当你使用字面量语法创建数组时,数组元素对象中不能有nil,否则会报错。
字面量字典
//字面量语法创建字典
NSDictionary *myDictionary = @{@"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3"};
//与之等价的方法
NSArray *keys = @[@"key1", @[@"key2", @"key3"]];
NSArray *objects = @[@"value1", @"value2", @"value3"];
NSDictionary *myDictionary = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
}
字面量语法的局限性:
字面量语法除字符串之外,所创建出来的对象必须属于Foundation框架才行。如果定义了这些类的子类,就无法使用字面量语法创建其对象。字面量通常只能用于特定的数据类型,比如数组和字典。不能使用字面量语法来创建自定义类的实例。
小结
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
多用类型常量,少用#define预处理指令
定义常量的命名
#define MAX_NUMBER 22
在实际的开发里面,这样定义出来的常量没有类型信息,并且假设此命令在某个头文件中,那么所有引入了这个头文件的的代码,其定义的固定值都会被这个替换掉。
类型常量的方法:
static const NSInteger kNumber = 22;
定义常量的位置方法
定义常量的位置是极其重要的,我们总喜欢在头文件里声明预处理指令,那么引入了这个头文件的所有文件都会含有这个变量,万一重名,程序变得异常麻烦。所以最好不要在头文件中定义常量,不论你是如何定义常量的,因为OC中没有“名称空间”这一概念。
因此我们最好在头文件中声明常量,在实现文件中定义常量
小结
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const来定义“只在编译单元内可见的常量”(translation-unit-specificconstant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
- 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
用枚举法表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
- 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
- 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
总结
从本周开始,笔者开始阅读Effective Objective-C 2.0,本周了解到OC的起源和部分代码编写时的优化,发现自己之前代码有很多不足,后续还会继续阅读这本书。