Objective-C’s duck typing system is both a blessing and a curse. A blessing, in that it’s amazingly flexible. A curse, in that such flexibility can lead to some awkward problems.
Something that typically happens in dealing with data from a property list, JSON or other similar format is that you perform some operation on an array of strings, only to find out that one of those strings was actually a dictionary. Boom – unrecognised selector sent to instance of NSCFDictionary. Unfortunately, in this case the magic smoke is escaping a long way from the gun barrel – we get to see what the problem is but not what caused it. The stack trace in the bug report only tells us what tried to use the collection that may have been broken a long time ago.
The easiest way to deal with bugs is to have the compiler catch them and refuse to emit any executable until they’re fixed. We can’t quite do that in Objective-C, at least not for the situation described here. That would require adding generics or a similar construction to the language definition, and providing classes to support such a construction. However we can do something which gets us significantly better runtime error diagnosis, using the language’s introspection facilities.
Imagine a mutable collection that knew exactly what kinds of objects it was supposed to accept. If a new object is added that is of that kind, then fine. If a new object of a different kind is added, then boom – invalid argument exception, crash. Only this time, the application crashes where we broke it not some time later. Actually, don’t imagine such a collection, read this one. Here’s the interface:
//
// GLTypesafeMutableArray.h
// GLTypesafeMutableArray
//
// Created by Graham Lee on 24/05/2010.
// Copyright 2010 Thaes Ofereode. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@class Protocol;
/**
* Provides a type-safe mutable array collection.
* @throws NSInvalidArgumentException if the type safety is violated.
*/
@interface GLTypesafeMutableArray : NSMutableArray {
@private
Class elementClass;
Protocol *elementProtocol;
CFMutableArrayRef realContent;
}
/**
* The designated initialiser. Returns a type-safe mutable array instance.
* @param class Objects added to the array must be an instance of this Class.
* Can be Nil, in which case class membership is not tested.
* @param protocol Objects added to the array must conform to this Protocol.
* Can be nil, in which case protocol conformance is not tested.
* @note It is impossible to set this object's parameters after initialisation.
* Therefore calling -init will throw an exception; this initialiser must
* be used.
*/
- (id)initWithElementClass: (Class)class elementProtocol: (Protocol *)protocol;
/**
* The class of which all added elements must be a kind, or Nil.
*/
@property (nonatomic, readonly) Class elementClass;
/**
* The protocol to which all added elements must conform, or nil.
*/
@property (nonatomic, readonly) Protocol *elementProtocol;
@end
Notice that the class doesn’t allow you to build a type-safe array then set its invariants, nor can you change the element class or protocol after construction. This choice is deliberate: imagine if you could create an array to accept strings, add strings then change it to accept arrays. Not only could you then have two different kinds in the array, but the array’s API couldn’t tell you about both kinds. Also notice that the added elements can either be required to be of a particular class (or subclasses), or to conform to a particular protocol, or both. In theory it’s always better to define the protocol than the class, in practice most Objective-C code including Cocoa is light in its use of protocols.
The implementation is then pretty simple, we just provide the properties, initialiser and the NSMutableArray primitive methods. The storage is simply a CFMutableArrayRef.
//
// GLTypesafeMutableArray.m
// GLTypesafeMutableArray
//
// Created by Graham Lee on 24/05/2010.
// Copyright 2010 Thaes Ofereode. All rights reserved.
//
#import "GLTypesafeMutableArray.h"
#import <objc/Protocol.h>
#import <objc/runtime.h>
@implementation GLTypesafeMutableArray
@synthesize elementClass;
@synthesize elementProtocol;
- (id)init {
@throw [NSException exceptionWithName: NSInvalidArgumentException
reason: @"call initWithClass:protocol: instead"
userInfo: nil];
}
- (id)initWithElementClass: (Class)class elementProtocol: (Protocol *)protocol {
if (self = [super init]) {
elementClass = class;
elementProtocol = protocol;
realContent = CFArrayCreateMutable(NULL,
0,
&kCFTypeArrayCallBacks);
}
return self;
}
- (void)dealloc {
CFRelease(realContent);
[super dealloc];
}
- (NSUInteger)count {
return CFArrayGetCount(realContent);
}
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
if (elementClass != Nil) {
if (![anObject isKindOfClass: elementClass]) {
@throw [NSException exceptionWithName: NSInvalidArgumentException
reason: [NSString stringWithFormat: @"Added object is not a kind of %@",
NSStringFromClass(elementClass)]
userInfo: nil];
}
}
if (elementProtocol != nil) {
if (![anObject conformsToProtocol: elementProtocol]) {
@throw [NSException exceptionWithName: NSInvalidArgumentException
reason: [NSString stringWithFormat: @"Added object does not conform to %s",
protocol_getName(elementProtocol)]
userInfo: nil];
}
}
CFArrayInsertValueAtIndex(realContent,
index,
(const void *)anObject);
}
- (id)objectAtIndex:(NSUInteger)index {
return (id)CFArrayGetValueAtIndex(realContent, index);
}
@end
Of course, this class isn’t quite production-ready: it won’t play nicely with toll-free bridging[*], isn’t GC-ready, and doesn’t supply any versions of the convenience constructors. That last point is a bit of a straw man though because the whole class is a convenience constructor in that it’s a realisation of the Builder pattern. If you need an array of strings, you can take one of these, tell it to only accept strings then add all your objects. Take a copy at the end and what you have is a read-only array that definitely only contains strings.
So what we’ve found here is that we can use the facilities provided by the Objective-C runtime to move our applications’ problems earlier in time, moving bug discovery from when we try to use the buggy object to when we try to create the buggy object. Bugs that are discovered earlier are easier to track down and fix, and are therefore cheaper to deal with.
[*]OK, so it is compatible with toll-free-bridging. Thanks to mike and mike for making me look that up…it turns out that CoreFundation just does normal ObjC message dispatch if it gets something that isn’t a CFTypeRef. Sadly, when I found that out I discovered that I had already read and commented on the post about bridging internals…