首页 Objective-C 从 C++ 到 Objective-C(10):实例化(续)

从 C++ 到 Objective-C(10):实例化(续)

7 2K

self = [super init...]

在上一篇提到的代码中,最不可思议的可能就是这句self = [super init...]。回想一下,self是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self必须要改变。我们将在下面解释为什么要这样做。

[super init]实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而, 有一个 API 可以用一个对象替换新分配的对象。Core Data(Apple 提供的 Cocoa 里面的一个 API)就是用了这种 API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承NSManagedObject类的时候,就需要仔细对待这种替换。在这种情形下,self就要指向两个对象:一个是alloc返回的对象,一个是[super init]返回的对象。修改self的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:

@interface B : A
{
int i;
}

@end

@implementation B

-(id) init
{
    // 此时,self 指向 alloc 返回的值
    // 假设 A 进行了替换操作,返回一个不同的 self
    id newSelf = [super init];
    NSLog(@"%d", i); // 输出 self->i 的值
    self = newSelf; // 有人会认为 i 没有变化
    NSLog(@"%d", i); // 事实上,此时的 self->i, 实际是 newSelf->i,
                     // 和之前的值可能不一样了
    return self;
}

@end
...
B* b = [[B alloc] init];

self = [super init]简洁明了,也不必担心以后会引入 bug。然而,我们应该注意旧的self指向的对象的命运:它必须被释放。第一规则很简单:谁替换self指针,谁就要负责处理旧的self指针。在这里,也就是[super init]负责完成这一操作。例如,如果你创建NSManagedObject子类(这个类会执行替换操作),你就不必担心旧的self指针。事实上,NSManagedObject的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?

初始化错误

初始化出错可能发生在三个地方:

  1. 调用[super init...]之前:如果构造函数参数非法,那么初始化应该立即停止;
  2. 调用[super init...]期间:如果父类调用失败,那么当前的初始化操作也应该停止;
  3. 调用[super init...]之后:例如资源分配失败等。

在上面每一种情形中,只要失败,就应该返回nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1, 3情况。要释放当前对象,我们调用[self release]即可。

在调用dealloc之后,对象的析构才算完成。因此,dealloc的实现必须同初始化方法兼容。事实上,alloc将所有的实例数据初始化成 0 是相当有用的。

@interface A : NSObject {
    unsigned int n;
}

-(id) initWithN:(unsigned int)value;
@end

@implementation A

-(id) initWithN:(unsigned int)value
{
    // 第一种情况:参数合法吗?
    if (value == 0) // 我们需要一个正值
    {
        [self release];
        return nil;
    }
    // 第二种情况:父类调用成功吗?
    if (!(self = [super init])) // 即是 self 被替换,它也是父类
        return nil; // 错误发生时,谁负责释放 self?
    // 第三种情况:初始化能够完成吗?
    n = (int)log(value);
    void* p = malloc(n); // 尝试分配资源
    if (!p) // 如果分配失败,我们希望发生错误
    {
        [self release];
        return nil;
    }
}
@end

将构造过程合并为alloc+init

有时候,allocinit被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到 Objective-C 的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似init,但要自己实现alloc。然而,这个对象需要注册到autorelease池中,除非发送retain消息,否则其生命周期是有限制的。以下即是示例代码:

// 啰嗦的写法
NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0.0f];
...
[tmp1 release];
// 简洁一些
NSNumber* tmp2 = [NSNumber numberWithFloat:0.0f];
...
// 无需调用 release

7 评论

woyaowenzi 2012年1月15日 - 21:37

"[super init] 实际上返回不同于当前对象的另外一个对象。"
这个没理解,既然在alloc的时候就已经申请过内存,为什么在初始化的时候还要再次申请一片新的内存?这样,如下代码被调用一次,
NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0.0f];
岂不是要先申请-->init内部释放-->再申请-->再在进行成员变量的初始化?

回复
DevBean 2012年1月16日 - 10:17

[alloc] 仅仅分配内存空间,[init] 则是初始化这个空间。也就是说,[init] 返回的是已经完成初始化的对象,[alloc] 是未初始化的对象,从这个意义上说,这是两个不同的对象。这就是前面一章中说的,C++ 中,内存分配和对象初始化是在一个构造函数中完成的,而 Objective-C 中则是两个不同的步骤:[alloc] 和 [init]。

回复
csland 2012年6月30日 - 17:23

一般情况下,init返回的和alloc返回的是同一个对象,只不过对象的内容被改变了。只有象单实例类才会出现真正不一致的情况,此时init返回的不是当前alloc的对象,而是一个全局的对象,此时要将alloc返回的对象删除。

回复
DevBean 2012年7月2日 - 09:14

感谢补充!

回复
woyaowenzi 2012年1月18日 - 09:52

这么说我就明白了。3Q。

回复
oak 2012年8月26日 - 21:15

初始化错误给定例子中第二种情况self似乎没有release?

回复
DevBean 2012年8月26日 - 23:09

这种情况下,由于不知道谁去负责 self 的释放,只能将释放交给外部调用的这个对象。

回复

回复 DevBean 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2