一、Objective-C的起源
Objective-C与C++、java等面向对象语言类似,不过很多方面有所差别。 该语言使用“消息结构”而非“函数调用”Objective-C语言有Smalltalk演化而来,后者是消息型语言的鼻祖。消息与函数调用之间的区别看上去就像这样:
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
object *obj = new Object;
obj -> perform(paremeter1, parameter2);
关键区别在于:消息结构的语言,其运行时所执行的代码有运行时环境来决定;而使用函数调用的语言,则由编译器决定。
Objective-C的重要工作都由“运行期组件”而非编译器来完成。运行期组件本质上就是一种与开发者锁边代码相链接的“动态库”,其代码能把开发者编写的所有程序粘合起来。这样一来,只需要更新运行期组件即可提升应用程序的性能。
Objective-C是C的“超集”,所以C语言中的所有功能在编写Objective-C代码是依然适用。
Objective-C语言中的指针是用来指示对象的。
例如想要声明一个变量,令其纸袋某个对象,可使用一下语法:
NSString *someString = @"The string";
其中指针someString在“栈”上,指向的实例 “The string”在堆上
所有的Objective-C的对象都需要这样声明,因为对象所占的内存总是分配在“堆空间”中,而绝不会分配在“栈”上。不能在栈中分配Objective-C对象,例如:
NSString stackString;
要点总结:
- Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型与指针。
二、在类的头文件中尽量少引入其他头文件
与C++一样,Objective-C也是用“头文件”与“实现文件”来区隔代码。用Obejctive-C语言编写“类”的标准方式为:以类名做文件名,分别创建两个文件,头文件后缀用.h,实现文件后缀用.m。创建好一个类之后,其代码看上去如下所示:
#import <Foundation/Foundation.h>
@interface EOCPerson: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
#import "EOCPerson.h"
@implementation EOCPerson
@end
但是有一种情况,比如你现在需要创建一个新类,名为EOCEmployer,在EOCPerson类的.h文件中声明一个EOCEmployer *类型的属性,那么,常规的做法是在EOCPerson.h文件中引用EOCEmployer.h文件#import "EOCEmployer.h" 但是,这种办法不够优雅,我们有一种方法叫做“向前声明”。代码如下:
#import <Foundation/Foundation.h>
@class EOCEmplyer;
@interface EOCPerson:NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
接下来,如果需要使用EOCEmployer类的头文件去实现某些东西,就可以在EOCPerson类的实现文件(.m文件)中引用EOCEmployer.h头文件,代码如下:
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
@end
次方法旨在将引入头文件的时机尽量延后,自在确有需要时才引入,这样就可以减少类的使用者所需引入的头文件数量。否则都是在.h文件中互相引用头文件的话只会一次引入那个头文件中的所有内容,如果次过程持续下去,则会引入许多根本用不到的内容,这当然会增加编译时间。
另外,还有一种特殊情况,就是如果有两个类,它们在各自的头文件中引入对方的头文件,就会导致“循环引用”。当解析其中一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件。使用#import而非#include指令虽然不会导致死循环,但这却意味着两个类里有一个无法被正确编译。对于此问题大家可以自己编写一个“循环引用”的示例尝试一下。
有时候,你写的类需要遵从某个协议,那么该协议必须有完整定义,且不能使用向前声明,因为向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。例如下方代码:
#import "EOCShape.h"
#import "EOCDrawable.h"
@interface EOCRectangle: EOCShape <EOCDrawable>
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
@end
由于需要遵从协议,所以上方代码中的第二个#import是难免的。鉴于此,最好的方法就是把协议单独放在一个头文件中。要是把EOCDrawable协议放在了某个打的头文件中,那么只要引入次协议,就必定会引入那个头文件中的全部内容,如此一来,就像上面说的那样,会产生相互依赖问题,而且还会增加编译时间。 然而有些协议,如“委托协议”,就不用单独写一个头文件了。在那种情况下,协议只有与接受协议委托的类放在一起定义才有意义。此时最好能在实现文件中声明此类实现了该委托协议,并把这段实现代码放在 分类 中。这样的话,只要在实现文件中引入包含委托协议的头文件接,而不需将其。放在公共头文件中。 所以,每次在头文件引入其他头文件之前,都要先问问自己这样做是否确有必要。如果可以用向前声明取代引入,那就不要引入。若因为要实现属性、实例变量或者要遵循协议而必须引入头文件,则应尽量将其移至 分类 中。这样做不仅可以缩减编译时间,而且还能降低彼此依赖程度。若是依赖关系过于复杂,则会给维护带来麻烦,而且,如果只想把代码的某个部分开放为公共API的话,台复杂的依赖关系也会出问题。
要点总结:
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
- 有时无法水用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至 分类 中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
三、多用字面量语法,少用与之等价的方法
(1)字面数值: 原始的标准的将数值类型封入对象中的方法如下:
NSNumber *someNumber = [NSNumber numberWithInt:1];
然而字面量方法如下:
NSNumber *someNumber = @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);
以字面量来表示数值十分有用。这样做可以令NSNumber对象变得整洁,因为声明中只包含数值,而没有多余的语法成分。 (2)字面量数组: 标准的创建数组方法如下:
NSArray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];
而使用字面量语法来创建则是:
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
上面这种做法不仅简单,而且还利于操作数组。数组的创建操作就是取某个下标所对应的对象,这用字面量来做更为容易。如果不用字面量,那么通常会用“objectAtIndex:”方法:
NSString *dog = [animals objectAtIndex:1];
若使用字面量,则是:
NSString *dog = animals[1];
这也叫“取下标”操作,更加简洁更易理解。 但是,如果用字面量方法创建数组的话,如果其中一个元素我们设置其为ni l的话,程序在运行时就会报错。 这样的好处就是可以及时修改自己在编写代码过程中的问题,如果用标准方法创建数组的话就不会因为元素为nil而报错,导致错误执行的程序难以找到问题之处。
(3)字面量字典: “字典”是一种映射型数据结构,可向其中添加键值对。与数组一样,OC代码也经常用到字典。其创建方式如下:
NSDictionary *personData = [NSDictionarydictionaryWithObjectsAndKeys: @"Matt", @"firstName", @"Galloway", @"lastName", [NSNumber numberWithInt:28], @"age", nil];
这样的写法比较困惑,因为其顺序是<对象>,<键>,<对象>, <键>。这与通常理解的顺序相反,所以不容易读懂。如果改为字面量方法,就清晰多了:
NSDictionary *personData = @{@"firstName" : @"Matt", @"lastName" : @"Galloway", @"age" : @28};
上方写法更加简明且易懂。左键右对的写法一目了然。 但是字面量字典也有一个问题就是当其中一个对象的值为nil的时候程序会报错,其优点与字面量数组一致。
一般情况下的访问字典方法如下:
NSString *lastName = [personData objectForKey:@"lastName"];
字面量语法如下:
NSString *lastName = persondata[@"lastName"];
明显这样子简单易懂。
要点总结:
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
四、多用类型常量,少用#define预处理指令
由于使用#define预处理指令的话经常会出现被别人修改后程序中所有的预处理指令值不唯一的情况,对程序运行造成影响,所以我们可以使用类型常量解决这个问题。 例如:
static const NSTimeInterval kAnimationDuration = 0.3;
上方的类型常量用于规定程序运行中动画的加载时间。但是以static 修饰的常量只能用于当前类的实现文件(.m文件)中。如果想要其他文件中的代码也可以使用该类型常量的话,则需要使用extern关键字修饰。
要点总结:
- 不要用与处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会据此执行查找与替换操作。及时有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
- 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
五、用枚举表示状态、选项、状态码
关于枚举的要点:
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为美剧类型,而多个选项又可以同时使用,那么就将各选项值定义为2的幂,以便于通过按位或操作将其组合起来。
- 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举时用开发者所选的底层数据类型实现出来的,二不会采用编译器所选的类型。
- 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
|