Sometimes an object can’t be initialized properly without some information from the method that is calling it. For example, imagine that an appliance can’t function without a name. (nil doesn’t count.) In this case, you need to be able to pass the initializer a name to use.
You can’t do this with init because, for now and always, init has no arguments. So you have to create a new initializer instead. Then, when another method creates an instance of Appliance, it would look like this:
Appliance *a = [[Appliance alloc] initWithProductName:@"Toaster"];
The new initializer for Appliance is initWithProductName:, and it accepts an NSString as an argument. Declare this new method in Appliance.h:
#import <Foundation/Foundation.h> @interface Appliance : NSObject { NSString *productName; int voltage; } @property (copy) NSString *productName; @property int voltage; - (id)initWithProductName:(NSString *)pn; @end
In Appliance.m, find the implementation of init. Change the name of the method to initWithProductName: and set productName using the passed-in value.
- (id)initWithProductName:(NSString *)pn { // Call NSObject's init method self = [super init]; // Did it return something non-nil? if (self) { // Set the product name [self setProductName:pn]; // Give voltage a starting value [self setVoltage:120]; } return self; }
Before you continue, build the project to make sure the syntax is right.
Now you can create an instance of Appliance with a given name. However, if you give Appliance.h and Appliance.m to another programmer, she may not realize she needs to call initWithProductName:. What if she creates an instance of Appliance in the most common way?
Appliance *a = [[Appliance alloc] init];
This is not an unreasonable action. As a subclass of NSObject, an instance Appliance is expected to do anything an instance of NSObject can do. And instances of NSObject respond to init messages. However, it causes a problem here because the above line of code creates an instance of Appliance that has nil for a product name and zero for voltage. And we decided earlier that every instance of Appliance needs a voltage of 120 and an actual name to function correctly. How can you prevent this from happening?
The solution is simple. In Appliance.m, add an init method to call initWithProductName: with a default value for the name.
- (id)init { return [self initWithProductName:@"Unknown"]; }
Notice that this new overridden init doesn’t do much work – it just calls the initWithProductName: method, which does the heavy lifting.
To test out your two initializers, you’ll need a description method. Implement description in Appliance.m:
- (NSString *)description { return [NSString stringWithFormat:@"<%@: %d volts>", productName, voltage]; }
Now, in main.m, exercise the class a bit:
#import <Foundation/Foundation.h> #import "Appliance.h" int main (int argc, const char * argv[]) { @autoreleasepool { Appliance *a = [[Appliance alloc] init]; NSLog(@"a is %@", a); [a setProductName:@"Washing Machine"]; [a setVoltage:240]; NSLog(@"a is %@", a); } return 0; }
Build and run the program.
Let’s take our exploration of initializers further. Create a new file: a subclass of Appliance named OwnedAppliance.
In OwnedAppliance.h, add a mutable set of owner names and three methods.
#import "Appliance.h" @interface OwnedAppliance : Appliance { NSMutableSet *ownerNames; } - (id)initWithProductName:(NSString *)pn firstOwnerName:(NSString *)n; - (void)addOwnerNamesObject:(NSString *)n; - (void)removeOwnerNamesObject:(NSString *)n; @end
Notice that one of the methods you’ve declared is an initializer that takes two arguments.
Implement the methods in OwnedAppliance.m:
#import "OwnedAppliance.h" @implementation OwnedAppliance - (id)initWithProductName:(NSString *)pn firstOwnerName:(NSString *)n { // Call the superclass's initializer self = [super initWithProductName:pn]; if (self) { // Make a set to hold owner names ownerNames = [[NSMutableSet alloc] init]; // Is the first owner name non-nil? if (n) { [ownerNames addObject:n]; } } // Return a pointer to the new object return self; } - (void)addOwnerNamesObject:(NSString *)n { [ownerNames addObject:n]; } - (void)removeOwnerNamesObject:(NSString *)n { [ownerNames removeObject:n]; } @end
Note that this class doesn’t initialize voltage or productName. The initWithProductName: in Appliance takes care of those. When you create a subclass, you typically only need to initialize the instance variables that you introduced; let the superclass take care of the instance variables that it introduced.
Now, however, you face the same situation as you did with Appliance and its superclass’s initializer, init. At the moment, one of your co-workers might create a terrible bug with this line of code:
OwnedAppliance *a = [[OwnedAppliance alloc] initWithProductName:@"Toaster"];
This code will cause the initWithProductName: method in Appliance to run. This method knows nothing about the ownerNames set, which means ownerNames will not get properly initialized for this OwnedAppliance instance.
The fix here is the same as before. In OwnedAppliance.m, add an implementation of the superclass’s initializer initWithProductName: that calls initWithProductName:firstOwnerName: and passes a default value for firstOwnerName.
- (id)initWithProductName:(NSString *)pn { return [self initWithProductName:pn firstOwnerName:nil]; }
Quiz time: Do you also need to implement init in OwnedAppliance? No. At this point, the following code will work fine:
OwnedAppliance *a = [[OwnedAppliance alloc] init];
Why? Because there is no implementation of init in OwnedAppliance, this line will trigger the init method implemented in Appliance, which calls [self initWithProductName:@"Unknown"]. self is an instance of OwnedAppliance, so it calls initWithProductName: in OwnedAppliance, which calls [self initWithProductName:pn firstOwnerName:nil].
What you wind up with is a chain of initializers that call other initializers.
Notice that Figure 29.3 shows one shaded initializer for each class. This initializer is the designated initializer for that class. init is the designated initializer for NSObject, initWithProductName: is the designated initializer for Appliance, and initWithProductName:firstOwnerName: is the designated initializer for OwnedAppliance. A class has only one designated initializer method. If the class has other initializers, then the implementation of those initializers must call (directly or indirectly) the designated initializer. Thus, the designated initializer acts as a funnel-point.
When you create a class whose designated initializer has a different name than its superclass’s designated initializer (as you did in Appliance and OwnedAppliance), you have a responsibility to document that in the header file. Add the appropriate comment in Appliance.h:
#import <Foundation/Foundation.h> @interface Appliance : NSObject { NSString *productName; int voltage; } @property (copy) NSString *productName; @property int voltage; // The designated initializer - (id)initWithProductName:(NSString *)pn; @end
and in OwnedAppliance.h:
#import "Appliance.h" @interface OwnedAppliance : Appliance { NSMutableSet *ownerNames; } // The designated initializer - (id)initWithProductName:(NSString *)pn firstOwnerName:(NSString *)n; - (void)addOwnerNamesObject:(NSString *)n; - (void)removeOwnerNamesObject:(NSString *)n; @end
Thus, we arrive at the rules that all stylish Objective-C programmers follow when writing initializers:
If a class has several initializers, only one should do the real work. That method is known as the designated initializer. All other initializers should call, either directly or indirectly, the designated initializer.
The designated initializer will call the superclass’s designated initializer before initializing its instance variables.
If the designated initializer of your class has a different name than the designated initializer of its superclass, you must override the superclass’s designated initializer so that it calls the new designated initializer.
If you have several initializers, clearly document which is the designated initializer in the header.