EGGER APPS

The Egger Apps Blog

16 Mar 2014

Objective-C: Typesafe Collection Access

Collection classes like NSArray, NSDictionary, NSSet or NSMapTable work with a single type: id. It's extremely convenient to be able to store different objects in the same collection. But sometimes it's also a bit dangerous.

I constantly write code like the following when dealing with data from external sources:

NSNumber *num = [dict objectForKey:@"age"];
if (![num isKindOfClass:[NSNumber class]]) num = nil;
NSInteger age = num.integerValue;

But even if you know the type of the object, it's annoying that you can't use property notation because the return type is id:

NSInteger age = [dict objectForKey:@"age"].integerValue; // compiler error

There are two ways we could fix this problem:

  1. Create a custom collection class that can only contain instances of a specific class. Since Objective-C doesn't support templates, this would mean you need A LOT of repeated code.
  2. Use special accessor methods that make sure you get an instance of a specific class.

A Typesafe Accessor

I'm going to consider only the second option, because I like the ability to mix and match objects of different classes in a single collection. The obvious solution for our case would be to write a category on NSDictionary like the following:

@implementation NSDictionary (TypesafeAccessors)
+(NSNumber*)numberForKey:(id)key  {
    NSNumber *number = [self objectForKey:key];
    return [number isKindOfClass:[NSNumber class]] ? number : nil;
}
@end

Problem solved! Now I can use code like this:

NSInteger age = [dict numberForKey:@"age"].integerValue; // safe!

However, going down this path would require us to write a method for every class we use!

Passing the class as a parameter

We can try to make it a bit more dynamic by passing the class as a parameter:

@implementation NSDictionary (TypesafeAccessors)
+(id)objectOfClass:(Class)aClass ForKey:(id)key  {
    id object = [self objectForKey:key];
    return [object isKindOfClass:aClass] ? object : nil;
}
@end

And we'd use it like this:

NSInteger age = [[dict objectOfClass:[NSNumber class] forKey:@"age"] integerValue];

This is type safe, but since the method returns id, the compiler doesn't know the class and we have to give up dot-nation.

Preprocessor Macros

But I really want to use dot-notation! So we need to somehow tell the compiler the class of the object! Since we don't have templates, we'll have to use an ugly preprocessor macro:

#define DictionaryObjectOfClassForKey(dict, aClass, key) \
({                                                       \
    aClass *obj = [dict objectForKey:key];               \
    [obj isKindOfClass:[aClass class]] ? obj : nil;      \
})

And we can use this macro:

NSInteger age = DictionaryObjectOfClassForKey(dict, NSNumber, @"age").integerValue;

For some reason I really don't like macros. They don't really look like real Objective C. I might be repeating myself, but I wish we had templates in Objective C.

Class methods and instancetype

There is one feature that's a bit similar to templates: instancetype. It works only with class methods, but after some tinkering I came up with a way to exploit this feature.

The trick is to implement the accessor as a class method in a category on NSObject.

@implementation NSObject (TypesafeAccessors)
+(instancetype)inDictionary:(NSDictionary*)dictionary forKey:(id)key  {
    id object = [dictionary objectForKey:key];
    return [object isKindOfClass:[self class]] ? object : nil;
}
@end

Sweet! Here's how our sample would look like:

NSInteger age = [NSNumber inDictionary:dict forKey:@"age"].integerValue;

The only nitpick is that you are calling a class method on NSObject, when the method should be implemented in NSDictionary from a semantic point of view.

Conclusion

The last two solutions are functionally equivalent. Both present a way to quickly access collections in a type safe manner. The macro is probably faster and semantically more correct, but I prefer the syntax of the class method.