Tuesday, June 16, 2009

Singleton Pattern in Cocoa

Peter Hosey recently tweeted:
From the “how NOT to implement a Cocoa singleton” dept: http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/CocoaObjects.html#//apple_ref/doc/uid/TP40002974-CH4-SW32

followed by:
If something over-releases an object, the bug is not in that object, and overriding methods in it to break retain/release is not fixing it.

The code sample in question is in the Apple documentation, and prescribes overriding the following methods for a singleton object: allocWithZone:, copyWithZone:, retain, release, autorelease, and retainCount. In addition, it also recommends implementing a sharedFloozit class method, so that users of your class are aware they're using a singleton. Go read that section, or you'll be lost for the rest of this post.

Really, I mean it. Go read that documentation. I'll be referring to it a lot.

That's a lot of methods to override just to implement a singleton. More to the point, a lot of it seems unnecessary. After all, as long as everyone else is doing their memory management correctly, retain counts shouldn't matter, right?

Wrong.

First, though, we need to get a definition straight. A singleton is a class of which there should only be one instance in any given process. There are actually very few singleton classes in the Cocoa framework including NSDocumentController and NSFontManager. [1] You cannot create more than one of these objects; if you try to call [[NSDocumentController alloc] init], you'll get back the exact same object as you do when you call [NSDocument sharedDocumentController], no matter how many times you call alloc and init. NSApplication is arguably a singleton as well; you can alloc another one, but you'll get an assertion failure when you call init.

Singletons are generally useful when initializing an object takes an inordinate amount of time. NSFontManager, for example, has to search several locations on the filesystem in order to do its job. You really don't want to be constantly initializing an NSFontManager. Further, it makes very little sense to have more than one instance of NSApplication within your application. Having one shared object for the whole system can simplify and optimize certain things. [2]

So that's a singleton. Only one instance allowed in the entire process.

There are other classes that have a +[SomethingClass sharedSomething] method or a +[SomethingClass defaultSomething] method. These methods provide access to an instance of the object intended to be shared by all users of the class, but do not prohibit the creation of another instance. Indeed, the Leopard Developer Release Notes specifically note that creating multiple instances of NSFileManager is possible and thread-safe, despite the existence of a globally-available object through +[NSFileManager defaultManager]. These are not singletons. If you're writing a class which provides a shared instance but doesn't not prohibit creation of other instances, then you should absolutely not override retain, release, autorelease, and retainCount, and should probably not override allocWithZone: either.

Most of the time, you don't need a singleton. Just the mention of a singleton is enough to get some people up in arms. But if you really truly need a singleton, then there's good reason for overriding the methods listed above.

Consider the following example:

MyFloozit *floozit1 = [[MyFloozit alloc] init];
[floozit1 doSomething];
[floozit1 release];

MyFloozit *floozit2 = [[MyFloozit alloc] init]; // MyFloozit is a singleton, so this should be the same object as floozit1
[floozit2 doSomething]; // CRASH HERE
[floozit2 release];


When floozit1 is set, a new MyFloozit is allocated, and a static MyFloozit pointer is set. When floozit1 is released, that static pointer is still pointing to the old instance. As a result, when we try to set floozit2 (or when anyone else tries to call [MyFloozit sharedFloozit]), we get back a pointer to that same instance. The one that has been dealloc-ed. Right there, despite following all the standard rules of memory management, you've crashed. The example might seem contrived, but if floozit1 and floozit2 are in separate methods (or separate threads calling the same method), this could be a very common scenario.

The point is, if you override allocWithZone: to force a class to be a singleton, then you must override release (and autorelease), or else anyone who believes they have ownership (after to calling init) will crash your program. Once you're disabling retain counting by overriding release, you might as well override retain and retainCount as well, to be consistent.

In the Apple Documentation linked above, there is a short paragraph right after the code showing the recommended way to override the various methods. It says:
Situations could arise where you want a singleton instance (created and controlled by the class factory method) but also have the ability to create other instances as needed through allocation and initialization. In these cases, you would not override allocWithZone: and the other methods following it as shown in Listing 2-15.
In this situation, I agree with Peter (and Apple) completely; don't override the memory management methods. I disagree with calling it a singleton, but whatever you want to call it, it's clear that you need those memory management methods.

If you think you need a singleton, think again. If you still think you need a singleton, bounce it off someone else to disabuse you of the notion. If you still need a singleton, then follow Apple's advice and override the memory management methods. Otherwise, you'll crash.

Of course, if you're using garbage collection, all retain count operations are no-ops, so none of this matters anyway. Feel free to go on your merry way, pitying the poor souls still living in a retain-counted world.

[1] The documentation referenced by Peter on "Creating a Singleton Instance" lists NSFileManager and NSWorkspace as examples of singletons, but this is incorrect. It is possible to create more than one instance of both these classes; Cocoa just happens to provide easy access to a shared instance which they suggest you use.

[2] It can also complicate a lot of things by introducing shared state. I'm not arguing for the use of the singleton pattern; I agree with Peter that most of the time, if you're using a singleton then you're "Doin It Rong". I'm simply arguing for Apple's implementation of the singleton pattern in the case where you really do need a singleton.