《Objective-C 编程》笔记

静态变量

// 声明静态变量
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];
}