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:
- 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.
- 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.