文章目录
前言
最近开始阅读一些iOS开发的相关书籍,第一本就是《Effective Objective-C 2.0》,这里对第一周的阅读内容进行简单归纳和总结,主要是熟悉OC语言。
了解OC语言的起源
OC语言由Smalltalk演化而来,其使用"消息结构"而非"函数调用"。
Smalltalk 起源于 20 世纪 70 年代的施乐帕洛阿尔托研究中心(Xerox PARC)。它是在当时计算机科学研究蓬勃发展的背景下诞生的,是面向对象编程(OOP)领域的先驱语言。
早期的 Smalltalk 主要用于研究和实验先进的编程概念。图构建一种能够模拟人类认知和交互方式的编程语言。例如,在当时传统的编程语言以过程式编程为主流时,Smalltalk 独树一帜地强调对象之间的消息传递。
对象是核心在 Smalltalk 中,所有的实体都是对象。无论是简单的数据类型,如整数、字符、布尔值,还是复杂的用户自定义的数据结构,都被视为对象。例如,一个简单的整数对象 "5",它不仅包含了数值本身,还包含了一系列可以对这个数值进行操作的方法。这些方法以消息传递的方式被调用,就好像这个整数对象能够 "理解" 并执行某些指令一样。
这种对象的概念是非常彻底的。以字符串对象为例,一个字符串对象可以接收诸如 "长度计算""字符提取""拼接其他字符串" 等消息,通过这些消息的传递来实现对字符串的各种操作。
消息传递机制消息传递是 Smalltalk 中对象之间交互的主要方式。当一个对象需要另一个对象执行某个操作时,它会向对方发送一个消息。例如,假设有一个表示图形的对象和一个表示绘图工具的对象。图形对象可能会向绘图工具对象发送一个 "绘制我" 的消息,绘图工具对象收到消息后,会根据图形对象的属性(如形状、颜色等)来执行绘制操作。
消息可以带参数,也可以不带参数。比如,一个数字对象向另一个数字对象发送 "加法" 消息时,会带上要相加的数值作为参数。这种机制类似于人类之间的交流,一个对象 "请求" 另一个对象做某事,使得代码的逻辑更加符合自然的思维方式。
类与继承类是创建对象的模板。在 Smalltalk 中,类定义了对象的属性和行为。例如,定义一个 "动物" 类,它可能包含 "名称""年龄" 等属性,以及 "移动""发出声音" 等行为(通过方法定义)。
继承是 Smalltalk 中实现代码复用和层次化设计的重要手段。例如,定义一个 "哺乳动物" 类继承自 "动物" 类,"哺乳动物" 类可以继承 "动物" 类的所有属性和行为,同时还可以添加自己特有的属性和行为,如 "喂奶" 等行为。这使得代码的组织结构更加清晰,符合现实世界中生物分类的逻辑。
对于消息和函数调用的区别,可以通过如下代码来体现:
objectivec
//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语言中的指针是用来指示对象的。想要声明一个变量。令其指代某个对象,可用如下语法:
objectivec
NSString *someString = @"The string";
上述代码声明了一个名为someString的变量,其类型为NSString*,即此变量为指向NSString的指针。所有的OC语言的对象声明必须以指针的形式,因为对象所占内存总是分配在"堆空间(heap space)"中,而绝不会在"栈(stack)"上。
someString变量指向分配在堆里的某块内存,其中含有一个NSString对象,若再创建一个变量,令其指向同一地址,如下:
objectivec
NSString *someString = @"The string";
NSString *anotherString = someString;
则并不会拷贝该对象,只是两个变量同时指向一个对象。即只有一个NSString实例,但有两个变量指向此实例,两个变量都是NSString类型。如图:
但是,在OC中有时会遇到定义里不带有的变量,它们可能使用到"栈空间",这些变量保存的不是CO对象。例如:
objectivec
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头文件:
objectivec
#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文件,就在两者之间建立了一种依赖关系。
如果过多地引入头文件,会导致代码的耦合性增加,还可能引入许多根本用不到的内容,会增加代码的编译时间和维护成本。
耦合性是指不同模块(在这里可以理解为不同的类)之间相互依赖的程度。例如,如果类 A 的头文件中引入了许多其他类的头文件,那么当这些被引入头文件中的类发生变化(如修改了成员变量或者方法签名)时,类 A 可能也需要进行相应的修改,这就增加了维护成本。
Person.h头文件:
objectivec
#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头文件:
objectivec
#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文件:
objectivec
#import "Person.h"
#import "Book.h"
@implementation Person
// 这里可以使用Book类的完整定义来实现Person类的方法
// 比如设置favoriteBook的属性等操作
@end
向前引用:
- 解决了头文件循环引用的问题
在复杂的代码结构中,尤其是涉及多个类或模块相互关联时,很容易出现头文件循环包含的情况。例如,类 A 的定义可能依赖于类 B 的部分信息,而类 B 的定义又依赖于类 A 的部分信息。如果直接使用头文件包含的方式,编译器会陷入无限循环地处理这些相互包含的头文件,造成循环引用。虽然我们使用#import而非#include不会导致死循环,但是这意味着两个类中有一个类无法被正确编译。
- 但当涉及协议(Protocol)相关操作时,进行向前声明就无法满足需求了。在 Objective - C 中,当一个类声明遵循某个协议(Protocol)时,编译器需要知道协议的完整定义来检查该类是否正确地实现了协议中的方法。这时就必须要引入头文件,而不能使用向前声明了。
小结
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类(使用@class),并在实现文件中引入那些类的头文件(使用import)。这样做可以尽量降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把"该类遵循某协议"的这条声明移至分类中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
多用字面量语法,少用与之等价的方法
objectivec
//字面量语法
NSString *someString = @"Effective Objective-C";
//与之等价的创建方法
NSString *someString = [[NSString alloc] initWithString:@"Effective Objective-C"];
字面量数值
objectivec
//字面数值
NSNumber *someNumber = @1;
//与之等价的数值创建方法
NSNumber *someNumber = [NSNumber numberWithInt:1];
能够以NSNumber实例表示的所有数据都可以使用该语法:
objectivec
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);
字面量数组
objectivec
//字面量语法
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,否则会报错。
因为在 Objective - C 中,使用字面量语法创建NSArray时,nil是一个特殊的值,它用于表示数组元素的结束标记,不能作为一个普通元素放在数组中间或者末尾。如果像这样把nil当作普通元素添加进去,会导致运行时错误,程序很可能会崩溃。因为系统在解析这个字面量创建的数组时,一旦遇到nil就会认为数组元素已经结束了,后面的内容不会再被当作数组元素处理了。在使用字面量语法创建字典时同理。
字面量字典
objectivec
//字面量语法创建字典
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预处理指令
定义常量的命名
objectivec
#define MAX_NUMBER 22
在实际的开发里面,这样定义出来的常量没有类型信息,并且假设此命令在某个头文件中,那么所有引入了这个头文件的的代码,其定义的固定值都会被这个替换掉。
类型常量的方法:
objectivec
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的起源和部分代码编写时的优化,发现自己之前代码有很多不足,后续还会继续阅读这本书。