This post describes a correction to the Be category when dealing with class clusters.
The Be category adds the
method to all objects and allows you to replace code like this:
MyObject *obj = [[[MyObject alloc] initWithThing:thing] autorelease];
with code like this:
MyObject *obj = [[MyObject be] initWithThing:thing];
This is useful in that it simplifies code and allows for some very nice memory management guidelines.
Unfortunately, as commenter David recently pointed out, there was a bug in my initial implementation. Â Specifically, some classes, such as NSArray, change what
is when their init method is called. Â In other words the two lines:
NSArray *myArray = [NSArray alloc]; myArray = [myArray init];
might point to different objects. Â Why is that?
Class Clusters
The main reason for this strange behavior is the implementation of a class cluster. A class cluster is a group of subclasses of a single abstract superclass. NSArray and NSString are examples of class clusters.
The problem they solve is this: sometimes, one implementation of a class is much more efficient than another, but only for certain data. For example, suppose we know an array will only ever have a single element. Then we can drop a lot of extra code and CPU cycles by utilizing that knowledge. So we get better performance out of a NSOneElementArray class. But clearly some arrays have more than one element, so we will still need something like an NSMultiElementArray class. I’m not sure if this is actually the class cluster behind NSArray, but it illustrates the idea that we get a specific subclass when we initialize from an abstract superclass. It is essentially a kind of factory method design pattern, if you’re into design patterns.
What doesn’t work
The old be implementation did this:
MyObject *obj = [[[MyObject alloc] autorelease] initWithThing:thing];
This works fine for all standard objects. Autorelease generally does not need to operate on an initialized object. It’s not exactly conservative code, but it works in practice on many classes.
However, it does not work as intended on class clusters.
Suppose MyObject is a class cluster. This is what happens:
MyObject *obj = [MyObject alloc]; // obj points to an allocated instance of the superclass obj = [obj autorelease]; // obj points to the same object, which is now autoreleased obj = [obj init]; // obj now points to something else, which is not autoreleased
The problem is that the autorelease was sent to the abstract superclass, an object that we no longer care about after the call to init.
The solution
Luckily, there is a way to fix this. We can use an NSProxy subclass. Specifically, I’ll use the BeProxy object, which expects the first-and-only method call to be to some init method. The return value of the init method becomes the object the user has a pointer to, and the BeProxy object gives us a place to call autorelease after the init method is called.
In other words, we can continue using the Be method exactly as we want to, and it works even in weird cases where init returns something other than what we started with.
If you’re curious about the details, check out the code here. Otherwise you can just scroll to the bottom to download the source files. As with all the code I post on this blog, it’s free to use without any warranty or strings attached.
One Trackback
[…] Commenter David found a bug in this implementation. Check out the fixed version here; the new source files are listed at the bottom of that post. (The fix doesn’t require any […]