静态变量
// 声明静态变量
static returnType varaibleName;
在不同的文件中共享变量不一定都是好事,有时会引起很严重的混淆问题。为此 C 语言引入了静态变量(static araible)概念。与全局变量一样,静态变量也要在函数外声明。不同的是,只有声明某个静态变量的文件才能访问该变量。这样,静态变量既保护了非局部的、存在于任何函数之外的优点,又避免了会被其他文件修改的问题。
结构体
// 声明 Person 结构
struct Person {
float height;
int weight;
};
类型为 Person 的变量是一个结构体,包含两个成员变量:身高 height 和体重 weight。
// 调用 Person 结构体
struct Person person;
person.height = 1.8;
person.weight = 96;
typedef
为了避免每次声明 Person 结构的时候都要调用 struct,为了简化输入,通常会使用 typedef 关键字为某个结构体声明一个新的类型。typedef 可以为某个结构声明等价的别名。
// 使用 typedef 的 Person 结构
typedef struct {
float height;
int weight;
} Person;
// 调用 Person 结构时
Person person;
person.height = 1.8;
person.weight = 96;
对象所有权
假设 A 对象和 B 对象之间具有父子关系,即 A 是 B 的唯一所有者,但 A 可以拥有许多类似 B 这种关系的子对象,也就是一对多的关系。
还原为编程问题:当且仅当 A 对象是另一个 B 对象的 holder 时,这个 A 对象的指针才会包含相应的 B 对象。
解决途径:在将子对象加入父对象的同时,设置指向父对象的指针。
优化:为了防止父子对象之间的互相强引用而导致 Retain 循环,必须把子对象中指向父对象的指针设为 weak,即保证只有父对象的指针才是强引用。
常量
#include 与 #import
#import 会确保预处理器只导入特定的文件一次,而 #include 允许多次导入同一个文件。
全局变量
编写 Objective-C 程序时,通常不是使用 #define 来保存全局变量。而是:
extern NSString const *globalVaraibleName;
const
表示,在程序的整个运行过程中,globalVaraibleName
指针的值不会发生改变。extern
关键字表示,globalVaraibleName
是存在的,但会在另一个文件中定义(起声明作用)。
enum
通过 enum 来定义一组常量,这与结构体类似。例如:
// 定义 enum。
enum Numbers {
one = 1,
two = 2,
three = 3,
four = 4
};
// 声明
enum Numbers num;
// 使用 typedef 来定义。
比较 #define 与全局变量
在即可以使用 #define
,也可以使用全局变量来定义常量的情况下(包括使用 enum
),为什么 Objective-C 程序员偏向于使用全局变量?
这是因为再某些情况下,使用全局变量的效率更高。例如,某个字符串都以一个固定的全局变量的形式出现,那么在进行字符串比较时,就可以使用 ==
,而不是使用 isEqual:
(算术运算要快于消息发送)。此外,再调试程序时,全局变量可以在调试器中看到值。
所以,在定义常量时应该使用全局变量和 enum
,而不是 #define
。
回调与对象所有权
三种回调类型:
- 对于只做一件事的对象,使用目标-动作对。
- 对于功能更复杂的对象(例如 NSURLConnection),使用辅助对象。最常见的辅助对象类型是委托对象(delegate)。
- 对于要出发多个回调的对象(例如 NSTimeZone),使用通知。
无论哪种类型的回调,如果代码不正确,那么都有可能使等待回调的对象得不到正确的释放。所以应当遵守以下规则:
- 通知中心不拥有其下的观察器。如果将某个对象注册成为观察器,那么应该在释放该对象时,将其移出通知中心:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 对象不拥有其下的委托对象或数据源对象。如果某个新创建的对象是另一个对象的委托对象或数据源对象,那么应该在其
dealloc
方法中取消相应的关联:
- (void)dealloc
{
[windowThatBossesMeAround setDelegate:nil];
[tableViewThatBegsForData setDataSource:nil];
}
- 对象不拥有其目标。如果某个新创建的对象是另一个对象的目标,那么该对象应该在其
dealloc
方法中将其相应的目标指针赋为 0:
- (void)dealloc
{
[buttonThatKeepsSendingMeMessages setTarget:nil];
}
init
大部分的类在创建实例的时候都会使用 [][Class alloc] init]
这种模式。这段代码调用了父类的 init 方法,以初始化父类的实例变量,并会得到一个指针指向初始化后的对象。通常情况下,这套调用模式可以满足绝大部分的需求,但在少数情况下,需要做一些特殊处理。比如:
- 出于优化的考虑,
init
方法会释放已经分配了内存的对象,然后创建一个新对象并返回之。 init
方法在执行过程中发生了错误,所以会释放对象并返回nil
。
对于第一种情况,Apple 建议的做法是:检查父类的初始化方法的返回值,确定不是 nil
并且有效。如果对象不存在,就没有必要执行自定义的初始化代码。
下面是 Apple 的建议写法:
- (id)init
{
// 调用 NSObject 的 init 方法
self = [super init];
// 检查父类的 init 方法的返回值是否为 nil
if (self) {
// 为 voltage 赋初始值
voltage = 120;
}
return self;
}
编写初始化方法时应当遵循以下规则:
- 如果某个类有多个初始化方法,那么应该由其中的一个方法来完成实际的任务。该方法称为指定初始化方法。其他的初始化方法都应该(直接地或间接地)调用指定的初始化方法。
- 指定初始化方法应该先调用父类的指定初始化方法,然后再对实例变量进行初始化。
- 如果某个类的指定初始化方法和父类的不同(这里指的是方法名不同),就必须覆盖父类的指定初始化方法,并调用新的指定初始化方法。
- 如果某个类有多个初始化方法,就应该在相应的头文件中明确的注明,哪个方法是指定初始化方法。
禁用 init 方法
比如有个 WallSafe 类,他的指定初始化方法是 initWithSecretCode:
。出于安全考虑,必须为 secretCode 赋上特定的值,而不是默认值。
最佳解决方法是覆盖父类的指定初始化方法,然后通过某种途径告之程序员不能调用这个方法,并提供修改建议:
- (id)init
{
@throw [NSException exceptionWithName:@"WallSafe 的初始化" reason:@"请使用 initWithSecretCode: 而不是 init" userInfo:nil];
}