Sunday, October 17, 2010

Handling Initialization Failure

A few months ago a friend of mine asked me the following question: "What should I do if I want to return nil in one of my initializers?" Returning nil in an initializer is nothing fancy. I do it all the time. However, there are a few rules you should be aware of.
  • Only return nil (in an initializer) if you can't perform the initialization. If a caller passes your initializer a path to a file that does not exist and the object can't work without a file it would make sense to return nil.
  • Don't return nil if you can find a nice "workaround" for a problem during initialization such as replacing a missing argument with a sensible default value.
  • If you want to return nil and you haven't sent a initialization message to super yet perform the following steps:
    • cleanup any resources you may have created,
    • call [self release]; and then
    • return nil
  • If you want to return nil and already have sent a initialization message to super (which returned nil) perform the following steps:
    • cleanup any resources you may have created,
    • return nil
If you return nil you must have a good reason. It is a good idea to let the caller know what went wrong during the initialization process. In Objective-C/Foundation we can simply use NSError to pass the cause of a problem back to the caller. Imagine a class called Person. A Person has a first and a last name. One can create a Person by using -initWithFirstName:lastName:error:. If the passed first and/or last name is nil -initWithFirstName:lastName:error: creates an error object, sends release to self and returns nil.

#import "Person.h"

enum {
   PersonErrorCodeFirstLastNameNotValid = 0
};
typedef NSInteger PersonErrorCodes;

@implementation Person

@synthesize firstName, lastName;

- (id)initWithFirstName:(NSString *)initFirstName 
               lastName:(NSString *)initLastName 
                  error:(NSError **)error {
   if(initFirstName == nil || initLastName == nil) {
      if(error != NULL) {
         NSMutableDictionary *u = [NSMutableDictionary dictionary];
         [u setValue:@"first/last name not valid." 
              forKey:NSLocalizedDescriptionKey];
         PersonErrorCodes c = PersonErrorCodeFirstLastNameNotValid;
         *error = [NSErrorerrorWithDomain:@"com.example.unique" 
                                     code:c 
                                 userInfo:u];
      }
      [self release];
      return nil;
   }
   self = [super init];
   if(self != nil) {
      self.firstName = initFirstName;
      self.lastName = initLastName;
   }
   returnself;   
}

- (id)initWithFirstName:(NSString *)initFirstName 
               lastName:(NSString *)initLastName {
   return [self initWithFirstName:initFirstName 
                         lastName:lastName 
                            error:NULL];
}

- (id)init {
   return [self initWithFirstName:[NSString string
                         lastName:[NSString string]];
}

- (void)dealloc {
   self.firstName = nil;
   self.lastName = nil;
   [super dealloc];
}

@end

As you can see, this class has three initializers: -init, -initWithFirstName:lastName: and -initWithFirstName:lastName:error:. The designated initializer is -initWithFirstName:lastName:error:, which is used by a caller which is interested in the cause of a failed initialization. -initWithFirstName:lastName: can be used by a caller which only wants to create a Person and see if it worked or not.