Home > Tips > Designated Initializers in Objective-C

Designated Initializers in Objective-C

September 21st, 2010

So, if you are a Mac/Objective-C developer, you are probably familiar with the Designated Initializer. The Designated Initializer (“DI”) is, in an Objective-C class, the one initializer in a class that is supposed to do the heavy lifting in initializing an object for use. Other initializers of the class should generally invoke the DI.

Similarly, subclasses of a given class should, in their DI, invoke the superclass DI (and the other initializers of the subclass should invoke the subclass DI). This way, any way the object gets initialized, the DI of both the superclass and the subclass are sure to be invoked.

The documentation does a decent job of explaining this, but I think it’s easy to miss an important detail, and that is that in a subclass, you must take care not only to invoke the superclass’ DI, but you also have to override it. (If your DI is different from the superclass’ DI, this is not necessarily obvious.)

For example, take these two classes:

@interface DJMySuperclass : NSObject
{
    DJMyInfo* importantData_;
    BOOL someImportantSwitch_;
}

// The designated initializer for DJMySuperclass:
- (id) initWithImportantData:(DJMyInfo*)importantData;

@end

@interface DJMySubclass : DJMySuperclass
{
    DJMoreInfo* moreImportantData_;
}

// The designated initializer for DJMySubclass:
- (id) initWithImportantData:(DJMyInfo*)importantData
           moreImportantData:(DJMoreInfo*)moreImportantData;

@end

Fairly straightforward so far, right? Now, take this implementation:

@implementation DJMySuperclass

- (id) init
{
    return [self initWithImportantData:nil];
}

- (id) initWithImportantData:(DJMyInfo*)importantData
{
    if (self = [super init]) {
        if (importantData) {
            importantData_ = [importantData retain];
        } else {
            importantData_ = [[DJMyImportantData alloc] init];
        }
        someImportantSwitch_ = YES;
    }
    return self;
}

- (void) dealloc
{
    [importantData_ release];
    [super dealloc];
}

@end

So far so good; completely obvious. Now let’s take this subclass implementation:

@implementation DJMySubclass

- (id) init
{
    return [self initWithImportantData:nil moreImportantData:nil];
}

- (id) initWithImportantData:(DJMyInfo*)importantData
           moreImportantData:(DJMoreInfo*)moreImportantData
{
    if (self = [super initWithImportantData:importantData]) {
        someImportantSwitch_ = NO;
        if (moreImportantData) {
            moreImportantData_ = [moreImportantData retain];
        } else {
            moreImportantData_ = [[DJMoreInfo alloc] init];
        }
    }
    return self;
}

- (void) dealloc
{
    [moreImportantData_ release];
    [super dealloc];
}

@end

Again, totally straightforward, right? So where is the bug?

Imagine this extension (or, maybe instead of a category extension, imagine a similar future addition to the superclass and subclass, a year down the road and done by a different developer).

@interface DJMySuperclass(JBJoeBlowsAdditions)

- (id) JB_returnsACopy;

@end

@implementation DJMySuperclass(JBJoeBlowAdditions)

- (id) JB_returnsACopy
{
    return [[[[self class] alloc] initWithImportantData:importantData_] autorelease];
}

@end

@implementation DJMySubclass(JBJoeBlowAdditions)

- (id) JB_returnsACopy
{
    DJMySubclass* other = [super JB_returnsACopy];
    [other->moreImportantData_ release];
    other->moreImportantData_ = [moreImportantData_ retain];
    return other;
}

@end

Where’s the bug?

The bug is that if you have a DJMySubclass object called foo, and you call DJMySubclass* bar = [foo JB_returnsACopy];, you’ll discover that bar->someImportantSwitch_ contains YES, when you thought you had a class invariant for DJMySubclass where someImportantSwitch_ was always forced to NO.

Why?

Because JB_returnsACopy in the superclass uses the initWithImportantData: initializer, and you didn’t cover it in DJMySubclass, so your designated initializer in the subclass never got invoked.

The fix is simple: cover the superclass’ DI in your subclass, and invoke your DI.

@implementation DJMySubclass

- (id) initWithImportantData:(DJMyInfo*)importantData
{
    return [self initWithImportantData:importantData
                     moreImportantData:nil];
}

@end

The documentation for Designated Initializers says:

It’s important to know the designated initializer when defining a subclass. For example, suppose we define class C, a subclass of B, and implement an initWithName:fromFile: method. In addition to this method, we have to make sure that the inherited init and initWithName: methods also work for instances of C. This can be done just by covering B’s initWithName: with a version that invokes initWithName:fromFile:.

General Principle: The designated initializer in a class must, through a message to super, invoke the designated initializer in a superclass.

This general principle, while true, is not sufficient. You really need to make sure to cover the superclass’ DI in your subclass, as described in that first paragraph, and shown in the accompanying diagram 3-3.

Categories: Tips Tags: ,
Comments are closed.