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 init
method, 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 _width
and _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 EOCSquare
instances, and so the _width
and _height
instance variables would not be decoded.
Things to Remember
Implement a designated initializer in your classes, and document which one it is. All other initializers should call through to this one.
If the designated initializer is different from the superclass, ensure that its designated initializer is overridden.
Throw an exception in initializers overridden from superclasses that should not be used in the subclass.