Next article: Friday Q&A 2009-12-04: Building Standalone iPhone Web Apps
Previous article: Friday Q&A 2009-11-20: Probing Cocoa With PyObjC
Tags: accessors cocoa objectivec
It's Black Friday, and that means it's time for another Friday Q&A. Today I'm going to talk about the use of accessors in Objective-C init/dealloc methods, a topic suggested by Jon Trainer.
Introduction
There has been a change in the Cocoa community in the past few years, where the use of accessors in init
/dealloc
is frowned upon, recommend against, and outright considered to be wrong. It is much better, they say, to directly access instance variables instead. In other words, the recommendation is to write code like this:
- (id)initWithWhatever: (id)whatever
{
if((self = [self init]))
{
_whatever = [whatever retain];
}
return self;
}
- (void)dealloc
{
[_whatever release];
[super dealloc];
}
- (id)initWithWhatever: (id)whatever
{
if((self = [self init]))
{
[self setWhatever: whatever];
}
return self;
}
- (void)dealloc
{
[self setWhatever: nil];
[super dealloc];
}
The pros of using accessors in
init
/dealloc
are pretty much the same as the pros of using them anywhere else. They decouple the code from your implementation, and in particular help you with memory management. How many times have you accidentally written code like this?
- (id)init
{
_ivar = [NSArray arrayWithObjects:...];
return self;
}
- (id)init
{
[self setIvar: [NSArray arrayWithObjects:...]];
return self;
}
Cons of Accessors
The downside to using accessors in this way can be summed up in one short sentence: accessors can have side effects. Sometimes these side effects are undesirable for init
/dealloc
.
When writing a setter, it needs to behave correctly if you're going to call it from init
/dealloc
. This means dealing with a partially constructed object.
Worse, if you ever override a setter in a subclass, you need to write it to handle the case where the superclass is using that setter to initialize or destroy its ivars. For example, this bit of innocuous-looking code is potentially dangerous:
- (void)setSomeObj: (id)obj
{
[anotherObj notifySomething];
[super setSomeObj: obj];
}
- (void)dealloc
{
[anotherObj release];
[super dealloc];
}
someObj
, then the override will execute after dealloc
has already executed, causing the override to access a dangling reference to anotherObj
, probably causing a nice crash.
It's not hard to fix this code to handle the situation gracefully. Simply assign anotherObj = nil
after releasing it in dealloc
, and everything works again. In general it's not difficult to make sure that your overrides behave properly, but if you're going to use accessors like this then you must remember to, and that is the difficult part.
Key-Value Observing
A topic which is frequently brought up in these discussions is key-value observing, because KVO is a common way for accessors to have side effects. Like other cases, if KVO side effects are triggered when the object is partially initialized or destroyed, and that code isn't written to tolerate such an object, bad things will occur. I personally think that it's mostly a red herring.
The reason it's mostly a red herring is because 99% of the time, KVO is not set up on an object until after it's already fully initialized, and is ceased before an object is destroyed. It is conceivable for KVO to be used such that it activates earlier or terminates later, but it's unlikely in practice.
It's not possible for outside code to do anything with your object until it's fully initialized, unless your initializer itself is passing pointers around to outside objects. And likewise, it's not possible for outside code to keep a KVO reference to your object as it's executing its dealloc
method, because it's too late for it to remove it, unless your dealloc
triggers something that allows it to do so. Superclass code does execute in these timeframes, but superclass code is extremely unlikely to do anything to cause external objects to observe properties that the superclass itself doesn't even have.
Conclusion
Now you know the pros and cons; should you use accessors in init
and dealloc
? In my opinion, it can go either way. The advantages aren't generally that great. The downsides are minor. I don't use accessors for the vast majority of my instance variables, but there are certain cases where the advantages become significant because I'm doing something special with an ivar, and in that case I don't hesitate to let the accessors save me some headache.
That's it for this week. Come back next week for another round. If you have a topic you would like to see covered here, send it in! Friday Q&A is driven by your suggestions, and the more I receive, the better topics you get to read about.
Comments:
IMHO the subclassing argument is a red herring as well. What it's saying is that you should complicate your classes, so that it's easier to subclass it. This misses a few important points:
1. You very rarely subclass
2. If you follow this logic you shouldn't call any of the methods from init/dealloc as someone might override them...
3. Subclassing is the tightest coupling possible between classes. It's hard to get right, and you should consider a lot of issues when overriding methods. So using ivars directly instead of accessors will not make subclassing significantly easier, just a tiny bit.
Also even if you subclass like this diagnosing the possible issues is not hard, and fixing it is easy as well. I think this is just a stupidly defensive programming, and the gains/losses are not in balance.
I think the ultimate question is this: does using accessors in init/dealloc increase or decrease coupling with the rest of your code? If it decreases it, then you should do it. If it decreases it, then you should not. And I think the answer to whether it increases or decreases coupling depends entirely on the situation at hand.
So your co-worker wrote a accessor that had a bug in it - and you crashed. Calling setFoo: with nil could happen anywhere - not just in dealloc. Your co-worker shouldn't write buggy code.
I'm in the "I used to use accessors, continued to use them after the apple warnings, then finally caved in" camp.
On the other hand, it royally bit me in the ass when I had a class that registered for KVOs after introspecting its property list, so that it could automatically do things with properties of subclasses. I think everyone can guess what happened there, it is basically the same problem that CoreData has internally, and they solved it with primative accessors.
The thing to understand is that (in CoreData paralance):
accessor == setValue:forObject:
no accessor == setPrimitave:valueForObject:
Unfortunately, primative accessors are not a concept builtin at the KVC/KVO level, I really wish they were. If you are using automatic KVO then the system could just build you @dynamic primative accessors for all your KVC compliant accessors.
[self setNeedsCaffeine];
Also, with GC, there is no dealloc and rarely a need for finalize, so half the problem is solved there. For init, the coding error in "Pros of Accessors" isn't an error at all. Point being, using ivars directly in GC is not as error prone as with non-GC.
dealloc
is that you can use the obj-c runtime to automatically discover and free them for you:
http://vgable.com/blog/2008/12/20/automatically-freeing-every-property/
That way you can't forget to
release
something, and it's less code.
But, I've gone to a
[ivar release], ivar = nil;
pattern after hearing Apple's recommendation. In my experience forgot-to-release-an-ivar bugs are relatively easy to track down and fix. But a side-effecting property being called in dealloc can be very nasty indeed!if ((self = [self init]))
But I can't figure out why. I went back to you post on initializers (http://mikeash.com/?page=pyblog/the-how-and-why-of-cocoa-initializers.html) and I see you did that there too, but I see no explanation why. I also tried looking up Apple's documentation, and they seem to use a single set of parentheses.
Is there any reason you use double parentheses?
http://www.kevincallahan.org/software/accessorizer.html
if(a = b)
It's a mistake. You actually wanted
==
, but forgot one of them.
Because this mistake is so common and so nasty, any reasonable compiler will catch it and warn about it. Gcc will do this if you compile with
-Wall
.
Of course, this is annoying when you actually want to assign in the if statement. The double parentheses just tell the compiler (and any humans reading the code) that you really did mean to assign, and not to compare.
In short, it is ALWAYS the subclasses responsibility to get the interaction to the superclass correct. I have found it is MUCH easier to do this by keeping Accessors simply that. Accessors. Likewise, doing code like:
- (id)initWithWhatever: (id)whatever
{
self = [self init];
[self setWhatever: whatever];
return self;
}
whenever I can (something you simply cannot do if you access the ivars directly) to really simplify the code substantially.
Add to this, the ease of memory management, and I see few reasons to NOT use the accessors at all times. Yes there are exceptions, but they are minor.
- (void)dealloc {
self.worker.delegate = nil;
self.worker = nil;
[super dealloc];
}
If this is from a subclass of NSThread, and you're non-GC, there might not be an autorelease pool in place to handle the retained and autoreleased 'worker' object. It doesn't matter if you use the new dot-notation or the normal brackets, i.e.,
[[self worker] setDelegate:nil]
, but the dot-notation makes it harder I think, to realize that you're actually using an accessor when resetting the worker's delegate.This is, incidentally, one of the major reasons why I never use dot notation.
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.