第三章 接口与API设计—第16条:提供"全能初始化方法"

所有对象均要初始化。在初始化时,有些对象可能无须开发者向其提供额外信息,不过一般来说还是要提供的。通常情况下,对象若不知道必要的信息,则无法完成其工作。以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。由于这样对象的创建成本较高,所以绘制表格时可依照标识符来复用,以提升程序效率。我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做"全能初始化方法"(designated initializer)。
如果创建类实例的方式不止一种,那么这个类就会有多个初始化方法。这当然很好,不过仍然要在其中选定一个作为全能初始化方法,令其他初始化方法都来调用它。NSDate就是个例子,其初始化方法如下:

- (id)init;
- (id)initWithString: (NSString *)string;
- (id)initWithTimeIntervalSinceNow: (NSTimeInterval)seconds;
- (id)initWithTimeInterval:(NSTimeInterval)seconds sinceDate:(NSDate *)refDate;
- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds;
- (id)initWithTimeIntervalSince1970:(NSTimeInterval)seconds;

正如该类的文档所述的那样,在上面几个初始化方法中,"initWithTimeIntervalSinceReferenceDate:"是全能初始化方法。也就是说,其余的初始化方法都要调用它。于是,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。
比如说,要编写一个表示矩形的类。其接口可以这样写:

#import <Foundation/Foundation.h>

@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
@end

根据第18条中的建议,我们把属性声明为只读。不过这样一来,外界就无法设置Rectangle对象的属性了。开发者可能会提供初始化方法以设置这两个属性:

- (id)initWithWidth:(float)width 
          andHeight:(float)height
{
    if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

可是,如果有人用[[EOCRectangle alloc] init]来创建矩形如何呢?这么做是合乎规则的,因为EOCRectangle的超类NSObject实现了这个名为init的方法,调用完该方法后,全部实例变量都将设为0(或设置成符合其数据类型且与0等价的值)。如果把alloc方法分配好的EOCRectangle交由此方法来初始化,那么矩形的宽度与高度就是0,因为全部实例变量都设为0了。这也可能正是你想要的结果,不过此时我们一般希望能自己设定默认的宽度与高度值,或是抛出异常,指明本类实例必须用"全能初始化方法"来初始化。也就是说,在EOCRectangle这个例子中,应该像下面这样,参照其中一种版本来覆写init方法:

//Using default values
- (id)init {
    return [self initWithWidth:5.0f andHeight:10.0f];
}

//Throwing an exception
- (id)init {
      @throw [NSException exceptionWithName: NSInternalInconsistencyException reason: @"Must use initWithWidth: andHeight: instead." userInfo: nil];
}

请注意,设置默认值的那个init方法调用了全能初始化方法。如果采用这个版本来覆写,那么也可以直接在其代码中设置_width与_height实例变量的值。然而,若是类的底层存储方式变了(比如开发者决定把宽度与高度一起放在某结构体中),则init与全能初始化方法设置数据所用的代码就都要修改。在本例这种简单的情况下没有太大问题,但是如果类的初始化方法有很多种,而且待初始化的数据也较为复杂,那么这样做就麻烦得多。很容易就忘了修改其中某个初始化方法,从而导致各初始化方法之间相互不一致。
现在假定要创建名叫EOCSquare的类,令其成为EOCRectangle的子类。这种继承方式完全合理,不过,新类的初始化方法应该怎么写呢?因为本类表示正方形,所以其宽度与高度必须相等才行。于是,我们可能会像下面这样创建初始化方法:

#import "EOCRectangle.h"

@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end

@implementation EOCSquare

- (id)initWithDimension:(float)dimension {
    return [super initWithWidth:dimension andHeight:dimension];
}

@end

上述方法就是EOCSquare类的全能初始化方法。请注意,它调用了超类的全能初始化方法。回过头看看EOCRectangle类的实现代码,你就会发现,那个类也调用了其初始化方法。全能初始化方法的调用链一定要维系。然而,调用者可能会使用"initWithWidth:andHeight:"或init方法来初始化EOCSquare对象。类的编写者并不希望看到此种情况,因为这样做可能会创建出"宽度"和"高度"不相等的正方形。于是,就引出了类继承时需要注意的一个重要问题: 如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法。在EOCSquare这个例子中,应该像下面这样覆写EOCRectangle的全能初始化方法:

- (id)initWithWidth:(float)width andHeight:(float)height {
    float dimension = MAX(width, height);
    return [self initWithDimension:dimension];
}

请注意看此方法是如何利用EOCSquare的全能初始化方法来保证对象属性正确的。覆写了这个方法之后,即便使用init来初始化EOCSquare对象,也能照常工作。原因在于,EOCRectangle类覆写了init方法,并以默认值为参数,调用了该类的全能初始化方法。在用init方法初始化EOCSquare对象时,也会这么调用,不过由于"initWithWidth:andHeight:"已经在子类中覆写了,所以实际上执行的是EOCSquare类的这一份实现代码,而此代码又会调用本类的全能初始化方法。因此一切正常,调用者不可能创建出边长不相等的EOCSquare对象。
有时我们不想覆写超类的全能初始化方法,因为那样做没有道理。比方说,现在不想令"initWithWidth:andHeight:"方法以其两参数中较大者作边长来初始化EOCSquare对象;反之,我们认为这是方法调用者自己犯了错误。在这种情况下,常用的办法是覆写超类的全能初始化方法并于其中抛出异常:

- (id)initWithWidth:(float)width andHeight:(float)height {
    @throw [NSException
        exceptionWithName:NSInternalInconsistencyException
                   reason:@"Must use initWithDimension: instead."
                 userInfo:nil];
}

这样做看起来似乎显得突兀,不过有时却是必需的,因为那种情况下创建出来的对象,其内部数据有可能相互不一致(inconsistent internal data)。如果这么做了,那么在EOCRectangle与EOCSquare这个例子中,调用init方法也会抛出异常,因为init方法也得调用"initWithWidth: andHeight:"。此时可以覆写init方法,并在其中以合理的默认值来调用"initWithDimension:"方法:

- (id)init {
    return [self initWithDimension:5.0f];
}

不过,在Objective-C程序中,只有当发生严重错误时,才应该抛出异常(参见第21条),所以,初始化方法抛出异常乃是不得已之举,表明实例真的没办法初始化了。
有时候可能需要编写多个全能初始化方法。比方说,如果某对象的实例有两种完全不同的创建方式,必须分开处理,那么就会出现这种情况。以NSCoding协议为例,此协议提供了"序列化机制"(serialization mechanism),对象可依此指明其自身的编码(encode)及解码(decode)方式。Mac OS X的AppKit与iOS的UIKit这两个UI框架都广泛运用此机制,将对象序列化,并保存至XML格式的"NIB"文件中。这些NIB文件通常用来存放视图控制器(view controller)及其视图布局。加载NIB文件时,系统会在解压缩(unarchiving)的过程中解码视图控制器。NSCoding协议定义了下面这个初始化方法,遵从该协议者都应该实现此方法:

- (id)initWithCoder:(NSCoder *)decoder;

我们在实现此方法时一般不调用平常所使用的那个全能初始化方法,因为该方法要通过"解码器"(decoder)将对象数据解压缩,所以和普通的初始化方法不同。而且,如果超类也实现了NSCoding,那么还需调用超类的"initWithCoder:"方法。于是,子类中有不止一个初始化方法调用了超类的初始化方法,因此,严格的说,在这种情况下出现了两个全能初始化方法.
具体到EOCRectangle这个例子上,其代码就是:

#import <Foundation/Foundation.h>

@interface EOCRectangle : NSObject <NSCoding>
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
- (id)initWithWidth:(float)width 
          andHeight:(float)height;
@end

@implementation EOCRectangle

// Designated initialiser
- (id)initWithWidth:(float)width 
          andHeight:(float)height
{
    if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

// Super-class’s designated initialiser
- (id)init {
    return [self initWithWidth:5.0f andHeight:10.0f];
}

// Initialiser from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {
    // Call through to super’s designated initialiser
    if ((self = [super init])) {
        _width = [decoder decodeFloatForKey:@"width"];
        _height = [decoder decodeFloatForKey:@"height"];
    }
}

@end

请注意,NSCoding协议的初始化方法没有调用本类的全能初始化方法,而是调用了超类的相关方法。然而,若超类也实现了NSCoding,则需改为调用超类的"initWithCoder:"初始化方法。例如,在此情况下,EOCSquare类就得这么写:

#import "EOCRectangle.h"

@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end

@implementation EOCSquare

// Designated initialiser
- (id)initWithDimension:(float)dimension {
    return [super initWithWidth:dimension andHeight:dimension];
}

// Super class designated initialiser
- (id)initWithWidth:(float)width andHeight:(float)height {
    float dimension = MAX(width, height);
    return [self initWithDimension:dimension];
}

// NSCoding designated initialiser
- (id)initWithCoder:(NSCoder*)decoder {
    if ((self = [super initWithCoder:decoder])) {
        // EOCSquare’s specific initialiser
    }
}

@end

每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,实现"initWithCoder:"时也要这样,应该先调用超类的相关方法,然后再执行与本类有关的任务。这样编写出来的EOCSquare类就完全遵守NSCoding协议了(fully NSCoding compliant)。如果编写"initWithCoder:"方法时没有调用超类的同名方法,而是调用了自制的初始化方法,或是超类的其他初始化方法,那么EOCRectangle类的"initWithCoder:"方法就没机会执行,于是,也就无法将_width及_height这两个实例变量解码了。

要点
在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。

  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容