第十六条 提供"全能初始化方法"

2017/8/25 posted in  第三章 接口与API设计

我们知道,所有的对象都要初始化但是有些对象可能有很多初始化方法(根据初始的信息来选择用哪个).我们以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象 时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。由于这种对象的创 建成本较高,所以绘制表格时可依照标识符来复用,以提升程序效率。我们把这种可为 对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)

我们看下面这个NSDate的例子:

-(id)init
-(id)initWithString:(NSString*)string
-(id)initWithTimelntervalSinceNow:(NSTimelnterval)seconds
-(id)initWithTimelnterval:(NSTimelnterval)seconds
                sinceDate:(NSDate*)refDate
-(id)initWithTimeIntervalSinceReferenceDate:(NSTimelnterval)seconds
-(id)initWithTimeIntervalSincel970:(NSTimelnterval)seconds

那么多的初始化方法中,我们要选一个全能初始化方法,让其他的初始化方法都来调用它。,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。

我们来用代码举例:

首先定义一个表示矩形的类:

#import <Foundation/Foundation.h>
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height; 
@end

然后定义一个初始化方法:

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

这样就会有一个问题,当有人用[[EOCRectanglealloc]init]来创建矩形时,因为NSObject中已经实现了init方法,如果把alloc方法分配好的EOCRectangle交由此方法来初始化,那么矩形的宽度与高度就是0,因为全部实例变量都设为0了。这种情况我们应该覆写init方法:

// Using default values
-(id)init {
    return [self initWithWidth:5.Of andHeight:10.Of];
)
// Throwing an exception
-(id)init {
    @throw [NSException
    exceptionWithName:NSInternalInconsistencyException 
    reason:@"Must use initWithWidth:andHeight: instead." 
    userInfo:nil];
}

还有一种情况,当我们创建名叫EOCSquare的类,令其成为EOCRectangle的子类时,新类的初始化方法写的时候要注意:

@import "EOCRectangle.h"
@interface EOCSquare : EOCRectangle 
-(id)initWithDimension:(float)dimension; 
@end

@implementation EOCSquare
-(id)initWithDimension:(float)dimension {
    return [super initwithwidth:dimension andHeightidimension];
}
@end

这了我们发现上面代码的初始化方法调用了父类的初始化方法,这样可能会导致一个问题:创建出一个”高度”和“宽度”不相等的正方形。所以:如果子类的全能初始化方法与超类方法的名称不 同,那么总应覆写超类的全能初始化方法。EOCSquare这个例子中,应该像下面这样覆写EOCRectangle的全能初始化方法:

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

覆写了这个方法之后,即便使用init来初始化EOCSquare对象,也能照常工作。原因在于, EOCRectangle类覆写了 init方法,并以默认值为参数,调用了该类的全能初始化方法。在用init方法初始化EOCSquare对象时,也会这么调用,不过由于“initWithWidth:andHeight:”已经在子类中覆写了,所以实际上执行的是EOCSquare类的这一份实现代码,而此代码又会调用本类的全能初始化方法。因此一切正常,调用者不可能创建出边长不相等的EOCSquare对象。

当然如果我们不想覆写父类的全能初始化方法,认为这是调用者自己犯了错误。在这种情况下,常用的办法是覆写超类的全能初始化方法并于其中抛出异常:

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

有时如果某个队形对象的实例有两种完全不同的创建方式,必须分开处理,所以就要编写多个全能初始化方法。只要记住每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,应该先调用超类的相关方法,然后再执行与本类有关的任务。

要点

  • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。

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