Before ARC, the main place for the programmer to make a memory management mistake was with respect to instance variables. Memory management of temporary variables within a single method is pretty easy; you can see the whole method at once, so now just follow the golden rule of memory management, balancing every retain, alloc, or copy with a release (or, if you’re returning an object with an incremented retain count, autorelease). But instance variables make things complicated, for many reasons:
Instance variables are persistent
Your own instance variables will persist when this method is over and your code has stopped running and the autorelease pool has been drained. So if you want an object value pointed to by an instance variable not to vanish in a puff of smoke, leaving you with a dangling pointer, you’d better retain it as you assign it to the instance variable.
Instance variables are managed from different places in your code
Memory management of an instance variable can be spread out over several different methods, making it difficult to get right and difficult to debug if you get it wrong. For example, if you retained a value assigned to an instance variable, you’ll later need to release it, conforming with the golden rule of memory management, to prevent a leak — but in some other method.
Instance variables might not belong to you
You will often assign to or get a value from an instance variable belonging to another object. You are now sharing access to a value with some other persistent object. If that other object were to go out of existence and release its instance variables, and you have a pointer to the instance variable value coming from that other object and you haven’t asserted your own ownership by retaining that value, you can wind up with a dangling pointer.
Thus, before ARC, the seemingly simple act of assigning an object to an instance variable could be fraught withperil. Consider this code:
NSMutableDictionary* d = [NSMutableDictionary dictionary];
// ... code that populates d goes here ...
self->_theData = d; // in non-ARC code this would be a bad idea!
Before ARC, that code constituted a serious potential mistake. If our code now comes to a stop, we’re left with a persistent pointer to an autoreleased object over which we have never asserted ownership; it might vanish, leaving us with a dangling pointer. The solution, obviously, is to retain this object as we assign it to our instance variable. You could do it like this:
[d retain];
self->_theData = d;
Or you could do it like this:
self->_theData = d;
[self->_theData retain];
Or, because retain returns the object to which it is sent, you could do it like this:
self->_theData = [d retain];
But none of those approaches is really satisfactory. Consider what a lot of trouble it will be if you ever want to assign adifferent value to self->_theData. You’re going to have to remember to release the object already pointed to (to balance the retain you’ve used here), and you’re going to have to remember to retain the next value as well. It would be much better to encapsulate memory management for this instance variable in an accessor (a setter). That way, as long as you always pass through the accessor, memory will be managed correctly. A standard template for such an accessor might look likeExample 12-4.
Example 12-4. A simple retaining setter
- (void) setTheData: (NSMutableArray*) value {
if (self->_theData != value) {
[self->_theData release];
self->_theData = [value retain];
}
}
In Example 12-4, we release the object currently pointed to by our instance variable (and if that object is nil, no harm done) and retain the incoming value before assigning it to our instance variable (and if that value is nil, no harm done either). The test for whether the incoming value is the very same object already pointed to by our instance variable is not just to save a step; it’s because if we were to release that object, it could vanish then and there, instantly turning value into a dangling pointer — which we would then, horribly, assign to self->_theData.
The setter accessor now manages memory correctly for us, provided we always use it to set our instance variable. This is one of the main reasons why accessors are so important! So the assignment to the instance variable in our original code should now look like this:
[self setTheData: d];
Observe that we can also use this setter subsequently to release the value of the instance variable and nilify the instance variable itself, thus preventing a dangling pointer, all in a single easy step:
[self setTheData: nil];
So there’s yet another benefit of using an accessor to manage memory.
Our memory management for this instance variable is still incomplete, however. We (meaning the object whose instance variable this is) must also remember to release the object pointed to by this instance variable at the last minute before we ourselves go out of existence. Otherwise, if this instance variable points to a retained object, there will be a memory leak. The “last minute” is typically dealloc, the NSObject method (Chapter 10) that is called as an object goes out of existence.
In dealloc, there is no need to use accessors to refer to an instance variable, and in fact it’s not a good idea to do so, because you never know what other side effects an accessor might have. And (under non-ARC code) you must always call super last of all. So here’s our pre-ARC implementation of this object’s dealloc:
- (void) dealloc {
[self->_theData release];
[super dealloc];
}
WARNING
Never, never call dealloc in your code, except to call super last of all in your override of dealloc. Under ARC, youcan’t call dealloc — yet another example of how ARC saves you from yourself.
You can see that, before ARC, memory management for object instance variables could be a lot of work! Not only did you need correctly written setter accessors for your instance variables, but also you had to make sure that every object of yours had a dealloc that released every instance variable whose value had been retained. This, obviously, was one more very good opportunity for you to make a mistake.
But wait, there’s more! What about an initializer that sets the value of an instance variable? Memory management is required here too. And you can’t use an accessor to help you; just as it’s not a good idea to use your own accessors to refer to your own instance variables in dealloc, so you should not use your own accessors to refer to your own instance variables in an initializer (see Chapter 5). The reason is in part that the object is not yet fully formed, and in part that an accessor can have other side effects.
To illustrate, I’ll rewrite the example initializer from Chapter 5 (Example 5-3). This time I’ll allow our object (a Dog) to be initialized with a name. The reason I didn’t discuss this possibility in Chapter 5 is that a string is an object whose memory must be managed! So, imagine now that we have an instance variable _name whose value is an NSString, and we want an initializer that allows the caller to pass in a value for this instance variable. It might look like Example 12-5.
Example 12-5. A simple initializer that retains an ivar
- (id) initWithName: (NSString*) s {
self = [super init];
if (self) {
self->_name = [s retain];
}
return self;
}
Actually, it is more likely in the case of an NSString that you would copy it rather than merely retain it. The reason is that NSString has a mutable subclass NSMutableString, so some other object might call initWithName: and hand you a mutable string to which it still holds a reference — and then mutate it, thus changing this Dog’s name behind your back. So the initializer would look like Example 12-6.
Example 12-6. A simple initializer that copies an ivar
- (id) initWithName: (NSString*) s {
self = [super init];
if (self) {
self->_name = [s copy];
}
return self;
}
In Example 12-6, we don’t bother to release the existing value of _name; it is certainly not pointing to any previous value (because there is no previous value), so there’s no point.
Thus, pre-ARC memory management for an instance variable may take place in as many as three places: the initializer, the setter, and dealloc. This is a common architecture! It is a lot of work, and a common source of error, having to look in multiple places to check that you are managing memory consistently and correctly, but that’s what you must do if you aren’t using ARC (though, as I’ll point out later in this chapter, at least Objective-C has the ability to write your accessors for you).