第三章 接口与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 

@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 

@interface EOCRectangle : NSObject 
@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这两个实例变量解码了。

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

  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

你可能感兴趣的:(第三章 接口与API设计—第16条:提供"全能初始化方法")