Skip to content

On Null Objects

I’ve said before, NSNull is an anti-pattern. It’s nice that we have the nil object, which allows us to have a stand-in for any object that doesn’t do anything. Unfortunately, it’s not a universal stand-in. You can’t add nil to a collection. You can add +[NSNull null] to a collection, but you can’t use that as a placeholder for other objects.

In either of these cases, you end up needing to warty your code. Either when you are building the collection, you must test for and handle trying to add nil. Or when you are reading the collection, you must test for and handle trying to use NSNull.

What would be nice is if we could easily define empty implementations of protocols. These would combine the two null implementations described above, along with more awesomeness:

  • Like nil, they provide default implementations of any method in the protocol.
  • Like NSNull, they can be used in collection classes.
  • Unlike both, they conform to the protocol for which you created them.

Then you can use these instances with aplomb, without your client code needing to work around their existence.

You’d use them like this:

        id <NSCacheDelegate> emptyCacheDelegate = [NullProtocol nullFor: @protocol(NSCacheDelegate)];
        NSArray *delegates = @[emptyCacheDelegate];

The nullFor: method would look to see whether you’ve previously created a Null Object for the specified protocol. Because they all do the same thing, it could use Flyweight instances.

+ (id)nullFor: (Protocol *)protocol
{
    NSString *className = [NSString stringWithFormat: @"Null%s", protocol_getName(protocol)];
    Class NullClass = NSClassFromString(className);
    NullProtocol *nullObject = objc_getAssociatedObject(NullClass, "instance");

If you don’t have one, then the method would create a new class pair, add this protocol to the class and instantiate it.

    if (nullObject == nil)
    {
        NullClass = objc_allocateClassPair(self, [className UTF8String], 0);
        class_addProtocol(NullClass, protocol);
        objc_registerClassPair(NullClass);
        nullObject = [NullClass new];
        nullObject->_protocol = protocol;
        objc_setAssociatedObject(NullClass, "instance", nullObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return nullObject;
}

The object could then use the standard message-forwarding technique to let nil handle all the messages from the protocol.

BOOL isNullMethodDescription(struct objc_method_description description)
{
    return (description.name == NULL && description.types == NULL);
}

- (struct objc_method_description)methodDescriptionForSelector: (SEL)aSelector inProtocol: (Protocol *)protocol
{
    //required methods
    struct objc_method_description description = protocol_getMethodDescription(protocol, aSelector, YES, YES);
    if (isNullMethodDescription(description))
    {
        //optional methods
        description = protocol_getMethodDescription(protocol, aSelector, NO, YES);
    }
    //look in the super-protocols
    if (isNullMethodDescription(description))
    {
        unsigned int protocolCount = 0;
        Protocol * __unsafe_unretained *protocols = protocol_copyProtocolList(protocol, &protocolCount);
        if (protocols == NULL)
        {
            return description;
        }
        unsigned int protocolCursor = 0;
        for (protocolCursor = 0; protocolCursor < protocolCount; protocolCursor++)
        {
            Protocol *thisProtocol = protocols[protocolCursor];
            description = [self methodDescriptionForSelector: aSelector inProtocol: thisProtocol];
            if (!isNullMethodDescription(description)) break;
        }
        free(protocols);
    }
    return description;
}

- (struct objc_method_description)methodDescriptionForSelector: (SEL)aSelector
{
    return [self methodDescriptionForSelector: aSelector inProtocol: _protocol];
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
    struct objc_method_description description = [self methodDescriptionForSelector: aSelector];
    return !(isNullMethodDescription(description));
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *superResult = [super methodSignatureForSelector: aSelector];
    if (superResult) return superResult;
    struct objc_method_description description = [self methodDescriptionForSelector: aSelector];
    return [NSMethodSignature signatureWithObjCTypes: description.types];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation invokeWithTarget: nil];
}

Oh wait, that all exists.

{ 3 } Comments

  1. Markus | July 22, 2012 at 8:42 am | Permalink

    There is one drawback with this solution though. You may end up with strange errors if your protocol has e.g. a “disabled” property and you pass the collection on to a method that tries to find all instances that are enabled:
    if (!obj.disabled) [self handleEnabledItem:obj]; // Null-object will be reported as enabled
    This way you will end up with the wrong outcome most likely. At least NSNull will throw exceptions for code that doesn’t support it, making it easy to spot it.

  2. Graham | July 22, 2012 at 10:13 am | Permalink

    “Software developer in ‘can think of edge case’ shocker. Film at 11.”

    Imagine a world where YES is used for positive results and NO is used for negative results. Then we’d design our API like this:

    @property (nonatomic, assign, getter=isEnabled) BOOL enabled;

  3. Markus | July 22, 2012 at 10:42 am | Permalink

    Sure, you can design your API around YES == positive, but there are existing APIs that are not, e.g. NSView or the -compare: style methods.

    I didn’t say it makes null-objects less viable than NSNull. I was merely arguing that you have to be careful how you use this pattern, as there might arise subtle issues if you pass an array with null-objects to some code which is not aware of it. It’s just deceptively easy to think “messaging to nil will always return failure so this is safe”.