On type safety and making it harder to write buggy code

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…

About Graham

I make it faster and easier for you to create high-quality code.
This entry was posted in code-level, iPad, iPhone, Mac. Bookmark the permalink.

5 Responses to On type safety and making it harder to write buggy code

  1. I like it. And for people worried about performance, you could always code it to only implement the type checking for non-release builds. Assuming your unit testing is exhaustive (you have unit checking right?) then any way the wrong type could be added to the array should be caught prior to release.

  2. Graham says:

    Mark, that’s fine when the type-safe array is only being used internally by the application. If you’re using it to build up a model from an external source such as a document file or network data, then you do still want to test that the input is valid as it may not be under your control. In that case, leaving the exceptions in and catching them is one possible solution.

  3. Fredrik Olsson says:

    This is a fun exercise in how to subclass NSMutableArray. But just as Apple’s documentation does, so would I like to discourage anyone from actually doing this in production code.

    The very first principle of Objective-C code, and most dynamic languages, is that methods are more important than classes. I do not care if you are a a Foo class or is Baring protocol compliant, only if you respond to the doBar: method.

    A type safe array is only good for catching bugs and crashing badly when the end user runs the code, and then it is too late. Does not matter how the error is caught, the app still goes down and the end user gets sad.

    You should instead use proper unit tests to verify that your model uses the correct data types. That way you will catch the errors before shipping to end users.

    Also any code not adding value to the end user value only serves to create a larger attack surface for security attacks, more bugs, and harder debugging.

  4. Graham says:

    Fredrik, as you gave almost exactly the same comment over at Stack Overflow, I sha’n’t repeat my comments there. However, I will note that Apple say “There is typically little reason to subclass NSArray. […] But there are situations where a custom NSArray object might come in handy.” so I don’t know where you got the idea that Apple discourage people from subclassing.

    NSArray and NSMutableArray are broken in that NSMutableArray is a subclass of NSArray when it shouldn’t be, it should be a Builder object for creating NSArrays. However, there are still times when you need both custom behaviour and to work within the existing pattern, and those times are when you might want to subclass the existing collection classes.

    Lastly I’ll note that in adding type safety to the collections, you’re reducing the attack surface and the chance for bugs. I’m not clear on where you think I’ve introduced problems, but would welcome specific criticism.

  5. Terry says:

    Frederik, there is no “first principle” of Objective-C. It’s a programming language, and it’s only goal is to result in well functioning software.

    To argue against a mature practice that tends to increase code stability is rather ludicrous. Stating that type safety is not necessary if you do TDD makes no sense — it’s like saying you don’t need to put a band-aid on a wound, as long as you can run a quarter mile.

    I want a toolbox that includes ALL practices that COULD result in better code. Writing concise, elegant code is tough enough without being hamstrung unnecessarily! :)

    Cheers

Comments are closed.