February 19, 2009

Objective-C Initializer Patterns

Creating objects in Objective-C is a two step process: you first allocate the space for the object, then you initialize it. In C++, the act of allocating (new) an object automatically calls the object's constructor, but it's basically the same two steps under the covers. However, the similarities end there. Objective-C initializer methods are different from C++ constructors. Those coming over from C++ might struggle a bit with them, so this article will attempt to bring forth these differences and discuss some common patterns.

Names

The most obvious difference are the names. The name of C++ constructors must be the same as the class name. Objective-C initializers, by convention, start with the prefix "init". However, since these are simply methods, this naming is purely by convention. You could call your initializer "jimmyJoeBob" if you wanted, but the pattern is to use "init" as the prefix, so "initJimmyJoeBob" would be OK.

Return Types

This one recently burned me. C++ constructors don't really have a return type, since they are not really methods. If it had to have a return type, you would assume it was a pointer to the class it is constructing. Not so in Objective-C. Take this blunder for instance: We have a base class:
@interface Base
{}
- (Base*) init;
@end
And we have a child class:
@interface Child : Base
{}
- (Child*) init;
@end
Each has an initializer. Neither is named "jimmyJoeBob", so we're OK there, but do you see the problem? Sure, this is a stupid mistake, but I believe many C++ converts will stumble into this: Class initializer methods must have a return type of id. If you fail to do this, and you are overriding a parent's initializer, you get a cryptic message from the compiler saying "warning: assignment from distinct objective-c type". This is because your override did not completely override; the return types don't match. Making all the initializers return id gets around this problem.

Return Values

All your initializers typically return "self". You might think that it would be nice if they did that automatically, but remember: these are really methods, not constructors. Initializers may return something different than what was allocated. What they return needs to be the same type as self, but it could be a different instance. One poor example would be if the class maintained a pool of already-allocated and initialized objects of itself. If the caller was initializing an object exactly the same as an object already in the pool, the initializer may deallocate itself and return the pooled instance instead. Initializers can also return nil if there was an error. C++ constructors can't return anything. The best they can do is set a flag, or worse, throw an exception. Because of these capabilities of the initializers, you should never use code like this:
id object = [MyClass alloc];
[object init];
[object doSomething];
The reason is the init method could have returned a different object, or nil, but this code would have missed it. The pattern you really should use is this:
id object = [[MyClass alloc] init];
And since the initializer could return nil, it might best to check the value before using it.

Automatic Memory Clearing

One really nice thing about Objective-C is that the allocate will fill the instance data with all 0's. As practical as this seems, C++ doesn't enjoy this luxury, and it must tediously initialize every instance variable; otherwise their values are random. This behavior makes most initializers unnecessary.

Super Initializers

Just like in C++, Objective-C initializers should explicitly call the appropriate initializer of its parent class. This is done via the "super" pointer. But wait! Since initializers can return nil or something entirely different, your child class initializer needs to be aware of that. The pattern here is to reassign self to what comes back from the super initializer, just in case it returned something else. And check for nil for good measure:
- (id) init {
  self = [super init];
  if(self) {
     ...
  }

  return self;
}

Returning Nil

When there is some error in the initializer, and your only recourse is to return nil, you have to be careful about creating a memory leak. If you simply returned nil, then the original pointer to the object is lost, which results in a memory leak. You should release yourself first before returning nil. The one who is returning nil is the one who releases. If you get back nil from a super initializer, you can assume it has already released. The release should be done at the first point of failure. Example:
- (id) init {
  if(someKindOfError) {
     [self release];
     return nil;
  }
}

Avoid Accessor Methods

It is generally recommended to avoid using property setters in initializers, due to possible side-effects. In other words, don't set properties with "self.propname = something". Use the instance variable directly: "propname = something;"

This can only apply to your own instance variables; for initializing instance variables on other objects, you may have only the accessor methods.

Convenience Constructors

There is one last pattern to consider for initializers. Some Cocoa classes have what are called convenience constructors. You might also call these factory methods. These are static (class-based) methods that allocate and initialize an object for you. Examples of these can be found in the NSString and NSArray classes. One thing to be aware of with convenience constructors: Unlike using alloc/init, the retain count of what is returned is not automatically set to 1. You need to retain any pointer you get back from a convenience constructor. I'm sure there is some valid use case as to why we have this inconsistent behavior, but I don't know what it is.

Example

Now we'll summarize all this with an example that demonstrates most of the patterns mentioned above. The interesting bits will be preceded by a red number and explained afterward:
- 1(id) 2initWithFile:(NSString*) filename {
3self = [super init];
4if(self) {
 if(filename == nil) {
     5[self release];
     return nil;
 } else {
     6m_filename = filename;
     [m_filename retain];
 }
}

7return self
}
  1. Initializers are instance methods that always return id
  2. By convention, initializers begin with the prefix "init"
  3. Always call your super initializer, and reassign self to that return
  4. Be sure to check the return value from your super, since it may return nil
  5. If you have an error release yourself first, then return nil
  6. Access your instance variables directly, and retain manually
  7. Return self

5 comments:

  1. Thanks, very succinct info. I am guilty of some of this!

    ReplyDelete
  2. I just want to know how do you declare m_filename in the header file

    ReplyDelete