[ObjC] hack to get type safe collections in ObjC
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.



by Joachim Bengtsson, 20120506

If you have every touched C++, Java or C# and then moved to Objective-C, you have at some point written one of the following in a source file:

NSArray/*<MyThing>*/ *_queuedThings;
NSDictionary *_thingMap; // contains MyThing
-(MAFuture*)fetchThing; // wraps a MyThing

With TCTypeSafety, you can write pretend that you're writing in a language with generics and write:

@interface MyThing : NSObject

NSArray<MyThing> _queuedThings;


Assume that we have some kind of Future class. Assume also that we have a factory that returns Futures that wrap the asynchronous creation of a MyThing. We might want to define the interface for such a factory like this:

@interface MyThingFactory : NSObject
- (TCFuture<MyThing> *)fetchLatestThing;

Later, when we use the factory:

MyThingFactory *fac = [MyThingFactory new];
NSString *thing = [fac fetchLatestThing].typedObject;

... actually generates a compiler warning, since typedObject returns MyThing, not NSString. (Note that we did not have to give TCFuture knowledge of the MyThing type to get this benefit, or modify it in any way except make it TCTypeSafety compatible).

This way, we can ensure that even though we have wrapped our MyThings in a TCFuture, we haven't thrown away type safety, so that if we want to change the return type of fetchLatestThing, we can just do so in the header and then fix all the compiler warnings, rather than going through every single usage of fetchLatestThing and fix any now invalid assumptions on the return type.

This is also useful for collections such as arrays and dictionaries:

NSMutableArray<MyThing> *typedArray = (id)[NSMutableArray new];
[typedArray insertTypedObject:@"Not a MyThing" atIndex:0]; // compiler warning! NSString ≠ MyThing
NSNumber *last = typedArray.lastTypedObject; // compiler warning! NSNumber ≠ MyThing
NSLog(@"Last typed thing: %@", last);


The syntax for indicating protocol conformance of a variable is the same that you would use for template/generics specialization in other language. Also, the namespace for protocols is separate from the namespace for classes, so we can have a protocol with the same name as a class. So if we implement a protocol with getters and setters that take and return the type that we are interested in, we can get type safety. For example, we can create the protocol MyThing as such:

@protocol MyThing
- (MyThing*)typedObjectAtIndex:(NSUInteger)index;
- (void)addTypedObject:(MyThing*)thing;

We then need to add support for these methods to NSArray and NSMutableArray. However, the type we are specializing on is only a compile time hint and does not affect the type of the instance, so in the implementation, we can just say that these return 'id'.

@implementation NSArray (SPTypeSafety)
- (id)typedObjectAtIndex:(NSUInteger)index;
    return [self objectAtIndex:index];

@implementation NSMutableArray (SPTypeSafety)
- (void)addTypedObject:(id)thing;
    [self addObject:thing];

Tada! Instant type safety.

There must be downsides.

Absolutely. You can only "specialize" on a single class: you can't create some generic facility that would let you specialize both the key and the value of an NSDictionary. You have to use weird selectors such as -[NSArray typedObjectAtIndex:], since the protocol conformance sadly does not override the method signature for your array instance, and using -[NSArray objectAtIndex:] will still give you type-unsafe return values.

Worst of all, by applying the TCMakeTypeSafe macro on your class, it will suddenly look like it has the interface of both a to-one accessor, NSArray, NSDictionary, and whatever else you add support for in TCMakeTypeSafe. Thus, you wouldn't get compile time warnings if you changed your NSArray into an NSDictionary, as you normally would. I think this trade-off is worth it, but I'm not sure.