Faux Collection Class Subclassing


A Very Special Guest Rant by AgentM, on March 16, 2006

Introduction

If you have used Cocoa a lot, you've come across a situation where you might like to subclass a so-called collection class. Apple decided that [[NSArray alloc] init] need not return NSArray. In fact, it never does. NSArray is a dud superclass of a bunch of various array storage implementations. This was done for performance reasons. For example, a very large array may be stored differently from a small array in order to save real memory.

Instead of encapsulating the various implementation details in one class, Apple (or more accurately, NeXT) decided that each implementation should get its own subclass. In the general case, you might end up with an NSCFArray or at Apple's whim, it could be something else. The grouping of the related classes together is called a class cluster in Apple terminology. Read the introductory documentation for more information.

The Problem

So why should we care? This optimization comes at a high cost- namely, breaking the basic object-oriented principle of inheritance. Because we can't rely on what is returned by [[NSArray alloc] init], we can't subclass NSArray and expect something useful, since NSArray is just a dud class.

In its infinite wisdom, Apple writes, Most developers would not have any reason to subclass NSArray. That is a naïve assumption at best. Personally, a subclassable NSArray, like any subclassable object, would allow me to add simple methods and instance variables particular to my situation. For example, in one project I use an NSArray to represent a message in a server-client protocol which can contain 32-bit integers, strings, and 16-bit integers. If I could have subclassed NSArray instead of just making it an instance variable, it would have prevented me from declaring and implementing a method that I could have inherited such as -addObject:.

So what is Apple's recommendation? Well, since NSArray is a dud class, subclassing it doesn't get you much, so Apple recommends implementing -count, -objectAtIndex:, the rest of the primitive methods plus code managing the retain count of the objects. Huh? That means a subclass of NSArray has essentially zero functionality to start with. So the only benefit to actually subclassing NSArray is that [mySubclassOfNSArrayInstance isKindOfClass:[NSArray class]] returns YES. Useless.

A Bad Hack

The whole subclassing issue irked me so I longed for a solution. There is a solution which does simulate subclassing using- surprise- -forwardInvocation:. Before you continue, I must warn you that the following code is rather error-prone, requires moderate low-level Objective-C knowledge, and uses a private method. I was mostly interested to see if it could be done at all. That said, it is an interesting solution and perhaps worth some discussion. It is heavily convoluted because NSArray doesn't have a designated initializer. Shameless.

The Goal

I would like a class that accepts all of the NSArray methods without an entirely new implementation of a basic array (as Apple suggests). A further subclass of that class should provide a class-safe array- an array which only accepts objects of a certain class. Such a statically-typed array would be similar to Java generics (but our solution checks types at run-time).

Additionally, I would like to be able to use this class wherever I might normally use NSMutableArray; so it must smell like an NSMutableArray subclass wherever one is needed.

MArray.h

Following is a hack to an inelegant problem. You have been warned.

Let us quickly define what it means to subclass in the most rudimentary sense. We can define subclassing as passing methods (at runtime, in Objective-C) to a superclass if the current class does not implement them. Objective-C allows for dynamic method dispatch at runtime, so one solution is to use -forwardInvocation:. Using a catch-all message dispatch, we can reroute the messages to the real array object and pass its return value back.

The basic principle is that if an object doesn't know how to respond to a selector, it can forward it through -forwardInvocation:. It sounds simple, and it is in the general case, except that the NSArray faux subclass has to catch the variety of possible-init... methods, since there is no designated initializer for NSArray. Great.

@interface MArray : NSProxy {
	id _proxiedObject;
}
-(id)proxiedObject;
@end

The only instance variable is the actual proxied object- in this case, an NSMutableArray. Currently, I only have one instance method: proxiedObject:, but imagine what would happen if I tried to compile against MArray.h using it as a subclass of NSMutableArray (which is my goal). The result would be a compiler warning every time I send an NSMutableArray message to the proxy. To squelch the warnings, I specify additional categories on MArray which are simply copied and slightly modified from NSArray.h:

@interface MArray (Basic)
- (unsigned)count;
- (id)objectAtIndex:(unsigned)index;
@end

@interface MArray (NSExtendedArray)

- (MArray *)arrayByAddingObject:(id)anObject;
- (MArray *)arrayByAddingObjectsFromArray:(NSArray *)otherArray;
...  
//snip a bunch of method declarations
@end

Note the adjustments I made in the above methods. I want the "subclass" to return instances of itself instead of NSArray. This isn't strictly necessary and will add some code complexity, but it will come in handy. (If you are really paying attention, you will notice that there is no longer a provision for an immutable array type from the MArray methods. This is an intentional design decision to reduce complexity in this example.)

MArray.m

Now we can shift gears to the meat-and-bones of the project. So far, there haven't been any real surprises; up ahead, we have a private method, weird release logic, and NSInvocation magic.

First, let us start with the standard forwarding mechanisms:

@implementation MArray
+(NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
	return [NSMutableArray methodSignatureForSelector:selector];
}

+(void)forwardInvocation:(NSInvocation*)invocation
{
	SEL selector;
	id ret,obj;
	NSString *selName;
	
	selector=[invocation selector];
	selName=NSStringFromSelector(selector);
	if(__MArrayPrint)
		NSLog(@"+forwardInvocation: %@",selName);
	//class creation method
	[invocation invokeWithTarget:[NSMutableArray class]];
	[invocation getReturnValue:&ret];
	obj=[[MArray alloc] _init_];
	[obj setProxiedObject:ret];
	[invocation setReturnValue:&obj];
}

The plus-signs on these methods are critical. Because NSMutableArrays have convenience class methods to create arrays, we also have to catch these at the class level on the proxy.

First, the +methodSignatureForSelector: returns NSMutableArray's signatures [this is only called whenever MArray doesn't implement such a class method].

Next, +forwardInvocation: forwards the message to [NSMutableArray class] and stores the result as the proxied object for this instance. So far, there isn't anything mysterious going on.

On to the instance methods:

-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
	if(__MArrayPrint)
		NSLog(@"-methodSig: %@",NSStringFromSelector(selector));
	return [NSMutableArray instanceMethodSignatureForSelector:selector];
}

-(void)forwardInvocation:(NSInvocation*)invocation
{
	NSString *sel;
	
	sel=NSStringFromSelector([invocation selector]);
	if(__MArrayPrint)
		NSLog(@"-forwardInvocation: %@",sel);
	//check if this is an initializer- intercept to return MArray
	if([sel hasPrefix:@"init"]
	   && strcmp([[invocation methodSignature] methodReturnType],@encode(id))==0)
	{
		id newProxiedObject;
		if(__MArrayPrint)
			NSLog(@"proxied -init");
		newProxiedObject=[NSMutableArray alloc];
		[invocation invokeWithTarget:newProxiedObject];
		[invocation getReturnValue:&newProxiedObject];
		[self setProxiedObject:newProxiedObject];
		[invocation setReturnValue:&self];
	}
	else
	{
		id ret;
		const char *retType;
		[invocation invokeWithTarget:_proxiedObject];
		//if the return type is an NSArray, make it an MArray instead
		retType=[[invocation methodSignature] methodReturnType];
		if(strcmp(retType,@encode(id))==0)
		{
			id newMArray;
			[invocation getReturnValue:&ret];
			if([ret isKindOfClass:[NSArray class]])
			{
				newMArray=[MArray arrayWithArray:(NSArray*)ret];
				[invocation setReturnValue:&newMArray];				
			}
		}
	}
}

The -methodSignatureForSelector: method is nearly the same as the class method except that it returns the instance's method signature.

Now the forwarding for the instance becomes a little more hairy because it detects whether this is an initializer (using a simple heuristic). If an initializer is detected, then it wraps the result into the MArray- this wraps the NSArray's private subclasses into a single proxy.

In addition, if the method is not an intializer but it returns an NSArray subclass, we also wrap that in a fresh MArray. This unifies the types of arrays I use, even though it might skew results from some methods such as objectAtIndex:. Ideally, I would create a list of methods which should wrap the return result, but this is good enough for now.

So now we are done with the actual message forwarding and wrapping but how does the memory management work? My solution is to implement -release in the NSProxy subclass, check the retainCount of the proxied object and, if it is about to be deallocated, -dealloc the instance of MArray.

-(void)release
{
	if(__MArrayPrint)
		NSLog(@"-release %d",[[self proxiedObject] retainCount]);
	if([[self proxiedObject] retainCount]-1==0)
	{
		if(__MArrayPrint)
			NSLog(@"final release for %@",self);
		[[self proxiedObject] release];
		[self dealloc];
	}
	else
	{
		[[self proxiedObject] release];
	}
}

So what happens when this object is sent to the autorelease pool? Because the proxy is actually added to the pool (instead of the proxied object), we will intercept the -release message and handle it no differently than the standard alloc,init lineup.

So that's the important bulk of MArray. There are about a dozen more methods which implement NSObject protocol conformance- download the project to get them; they are merely one-liners.

Try it Out

So what can we do with new proxy class? Everything a regular NSMutableArray can do. Here are some examples:

    MArray *m2;
    m2=[[MArray alloc] initWithObjects:@"a",@"b",nil];	

    [m2 addObject:@"c"];
    NSLog(@"m %@",m2);

    [m2 release];
	
    m2=[MArray arrayWithObjects:@"1",@"2",nil];
    [m2 addObject:@"3"];
    NSLog(@"%@",m2);
	
    NSLog(@"isEqual %d",[[MArray arrayWithObjects:@"1",@"2",nil] isEqual:[MArray arrayWithObjects:@"1",@"2",nil]]); 
    //YES
    
    NSLog(@"isKindOfClass:NSMutableArray %d",[[MArray arrayWithObjects:@"1",@"2",nil] isKindOfClass:[NSMutableArray class]]); 
    //YES
	
    NSLog(@"isKindOfClass:NSArray %d",[[MArray arrayWithObjects:@"1",@"2",nil] isKindOfClass:[NSArray class]]); 
    //YES
    
    NSLog(@"isMemberOfClass:NSMutableArray %d",[[MArray arrayWithObjects:@"1",@"2",nil] isMemberOfClass:[NSArray class]]); //NO
    NSLog(@"isProxy %d",[[MArray arrayWithObjects:@"1",@"2",nil] isProxy]); 
    //YES
    
    NSLog(@"respondsToSelector:count %d",[[MArray arrayWithObjects:@"1",@"2",nil] respondsToSelector:@selector(count)]); 
    //YES
	
    NSLog(@"superclass %@",[[MArray arrayWithObjects:@"1",@"2",nil] superclass]); 
    //%NSCFArray -- perhaps we could do better here, but it is true

The code above, when compiled, does not emit any warnings because I added additional categories on MArray which I have not shown here.

Especially important to note is that an MArray instance is returned from the initializers and convenience initializers. Since any private subclasses are wrapped in this proxy, we may now subclass the proxy, effectively inheriting NSMutableArray methods and the man-behind-the-curtain is virtually transparent.

MTypedArray.h

Now that we have a class that behaves like a subclass of NSMutableArray, we can create additional subclasses that add the array behavior we have always wanted. In this example, I create a subclass called MTypedArray which behaves similarly to generics-specified arrays in other languages. Specifically, this subclass will only allow objects of certain types to be added to it.

#import 
#import "MArray.h"

@interface MTypedArray : MArray {
	Class _class;
}
-(id)initWithTypeClass:(Class)class;
-(void)addObject:(id)object;
-(Class)typeClass;
@end

The type class which will be permitted for this collection class will be set at init time and remain immutable.

Next, I need to identify all methods which allowed insertion into the array and overload them to test the objects for class compliance. NSMutableArray has about half-a-dozen such methods, but for the purpose of this example, I will overload one of them: addObject:.

MTypedArray.m

The implementation is mercifully simple. In fact, it is exactly how I would want to implement a direct subclass of NSArray if it were meaningful.

@implementation MTypedArray
-(id)initWithTypeClass:(Class)class
{
    if(self=[super initWithCapacity:5])
    {
        _class=class;
    }
    return self;
}

-(void)addObject:(id)object
{
    if(![object isKindOfClass:_class])
        [NSException raise:NSInvalidArgumentException format:@"Instance of MTypedArray takes \'%@\' only!",_class];
    [[self proxiedObject] addObject:object];
}

-(Class)typeClass
{
    return _class;
}
@end

That's it! If the object to be added doesn't meet my criterion, I throw an exception.

Now I have demonstrated how an NSArray subclass might be useful. Here are some other examples of cases where you might use an array subclass:

A Problem with NSProxy's Meta-class

While coding up this project, I came across a strange quirk which I documented more in an email to the cocoa-dev mailing list hosted by Apple. The problem is that +[NSProxy class] returns an NSProxy. This is surprising, albeit documented, behavior. This causes problems because suddenly, one can no longer rely on basic object functionality. For example, this fails:

NSLog(@"%@",[NSProxy class]);

with an "unrecognized selector" exception. This is a problem in the MArray case because it is meaningful to be able to print the class name of the something that looks like an NSMutableArray subclass. Implementing +description doesn't cut it. Instead, one has to implement a private method:

@implementation NSProxy (Description)
+(NSString*)_copyDescription
{
	return [NSStringFromClass([self class]) copy];
}
@end

NSSStringFromClass() returns the name of the class regardless of whether or not the object in question is in fact an NSObject subclass.

Some Improvements to be Made

There are certainly improvements that can be made to this code. Here is a list of ideas:

Summary

Using an NSProxy subclass, it is possible to create an object which resembles an NSMutableArray subclass. The result is satisfactory but error-prone. If the private subclasses of NSArray and NSMutableArray didn't exist, this hack would not either. But even with an NSProxy subclass, it is possible to provide a virtually seamless class that meets the criteria and doesn't produce compiler warnings.

Using this code as a springboard, any Apple collection class could be wrapped like this.

Download the Sources

MArray Xcode Project

N.B.

If you have questions or suggestions, email me. I am also a member of CocoaHeads, so stop by one of our chapters and get involved!

This article is published as information in the public domain.



borkware home | products | miniblog | rants | quickies | cocoaheads
Advanced Mac OS X Programming book

webmonster@borkware.com