Effective Objective-C 2.0: Item 16:Designated Initializer

Item 16: Have a Designated Initializer

All objects need to be initialized. When initializing an object, you sometimes don’t need to give it any information, but often you do. This is usually the case if the object cannot perform its action without knowing this information. An example from UIKit, the iOS UI framework, is UITableViewCell, which requires being told its style and an identifier to group cells of different types, enabling efficient reuse of cell objects that are expensive to create. The term given to the initializer that gives the object the required amount of information to perform its task is the “designated initializer.”

If there is more than one way to create an instance of a class, the class may have more than one initializer. This is perfectly fine, but there should still be just the one designated initializer through which all other initializers call. An example of this isNSDate, which has the following initializers:

- (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

The designated initializer in this case isinitWithTimeIntervalSinceReferenceDate:, as explained in the documentation for the class. This means that all the other initializers call through to this initializer. Thus, the designated initializer is the only place where internal data is stored. If the underlying data store is changed for any reason, only one place needs to be changed.

For example, consider a class that represents a rectangle. The interface would look like this:

#import <Foundation/Foundation.h>

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

Taking heed of Item 18, the properties are read-only. But that means that there’s no way in which a Rectangle object can ever have its properties set externally. So you might introduce an initializer:

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

But what if someone decides to create a rectangle by calling [[EOCRectangle alloc] init]? Doing so is legitimate, since EOCRectangle’s superclass, NSObject, implements a method called init, which simply sets all instance variables to 0 (or equivalent of 0 for that data type). If that method is invoked, the width and height will stay set at 0 from when the EOCRectangle instance was allocated (in the call toalloc) in which all instance variables are set to 0. Although this might be what you want, you will more likely want to either set the default values yourself or throw an exception to indicate that an instance must be intialized using your designated initializer only. In the case of EOCRectangle, this would mean overriding the initmethod, like so:

// 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];

}

Note how the version that sets default values calls straight through to the designated initializer. This version could have been implemented by directly setting the _widthand _height instance variables. However, if the storage backing the class had changed—for example, by using a structure to hold the width and height together—the logic of how to set the data would have to be changed in both initializers. This wouldn’t be too bad in this simple example, but imagine a more complicated class with many more initializers and more complicated data. It would be very easy to forget to change one of the initializers and end up with inconsistencies.

Now imagine that you want to subclass EOCRectangle and create a class calledEOCSquare. It’s clear that this hierarchy makes sense, but what should the initializer be? Clearly, the width and height should be forced to be equal, since that’s what a square is! So you might decide to create an initializer like so:

#import "EOCRectangle.h"

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

@implementation EOCSquare

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

@end

This would become EOCSquare’s designated initializer. Note how it calls its superclass’s designated initializer. If you look back to the implementation ofEOCRectangle, you’ll see that it too calls its superclass’s designated initializer. This chain of designated initializers is important to maintain. However, it’s still possible for callers to initialize an EOCSquare object by using either theinitWithWidth:andHeight: or the init method. You don’t really want to allow this, since someone could end up creating a square whose width and height don’t match. This illustrates an important point when subclassing. You should always override the designated initializer of your superclass if you have a designated initializer with a different name. In the case of EOCSquare, you could override EOCRectangle’s designated initializer like so:

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

Note how the designated initializer of EOCSquare has been called. In implementing this, the method called init will also just magically work. Recall how inEOCRectangle, it was implemented to call through to EOCRectangle’s designated initializer with default values. It will still do this, but since theinitWithWidth:andHeight: method has been overridden, EOCSquare’s implementation will be called, which in turn calls through to its designated initializer. So everything will still work, and there’s no way of creating an EOCSquare with unequal sides.

Sometimes, you won’t want to override the superclass’s designated initializer, as it doesn’t make sense. For example, you might decide that it’s not very nice that anEOCSquare object created with the initWithWidth:andHeight: method has its dimension set to the maximum of the two values passed in. You might decide that it’s a user error instead. In this case, the common pattern is to override the designated initializer and throw an exception:

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

This might seem harsh, but it’s sometimes required, since it would leave an object being created with inconsistent internal data. In the case of EOCRectangle andEOCSquare, it would mean that calling init would also cause the exception to be thrown, because init calls through to initWithWidth:andHeight:. In that case, you might decide to override init and call initWithDimension: with a sensible default:

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

However, throwing an exception in Objective-C should mean that the error is fatal (see Item 21), so throwing an exception from an initializer should be a last resort if the instance cannot be initialized otherwise.

In some situations, you may need to have more than one designated initializer. These situations arise when an instance of the object can be created in two distinct ways and must be handled separately. An example of this is the NSCoding protocol, a serialization mechanism that allows objects to dictate how they should be encoded and decoded. This mechanism is used extensively throughout AppKit and UIKit, the two UI frameworks from Mac OS X and iOS, respectively, to provide the ability for objects to be serialized using an XML format called a NIB. These NIBs are usually used to store a view controller’s layout of its views. When loaded from a NIB, the view controller goes through the unarchiving process.

The NSCoding protocol defines that the following initializer method should be implemented:

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

This method can’t usually call through to your main designated initializer, because it has to do different work to unarchive the object through the decoder. Also, if the superclass implements NSCoding as well, its initWithCoder: method needs to be called. You would end up with two designated initializers in the strict sense of the term, because more than one initializer calls through to a superclass initializer.

Applying this to EOCRectangle would give the following:

#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 initializer
- (id)initWithWidth:(float)width
          andHeight:(float)height
{
    if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

// Superclass's designated initializer
- (id)init {
    return [self initWithWidth:5.0f andHeight:10.0f];
}

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

@end

Note how the NSCoding initializer calls through to the superclass designated initializer rather than one of its own. However, if the superclass also implements NSCoding, it would call through to that initializer instead. For example, consider EOCSquare:

#import "EOCRectangle.h"

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

@implementation EOCSquare

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

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

// NSCoding designated initializer
- (id)initWithCoder:(NSCoder*)decoder {
    if ((self = [super initWithCoder:decoder])) {
        // EOCSquare's specific initializer
    }
    return self;
}

@end

In the same way that any initializer is chained through to the superclass implementation, initWithCoder: calls it first before continuing to do anything specific that it needs to do. In this way, EOCSquare is also fully NSCoding compliant. If you called through to your own initializer here or to a different superclass initializer, EOCRectangle’s initWithCoder: would never get called for EOCSquareinstances, and so the _width and _height instance variables would not be decoded.

Things to Remember

Image Implement a designated initializer in your classes, and document which one it is. All other initializers should call through to this one.

Image If the designated initializer is different from the superclass, ensure that its designated initializer is overridden.

Image Throw an exception in initializers overridden from superclasses that should not be used in the subclass.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值