Faking "switch" with an object value

Jeff Kelley started a thread on the objc-language mailing list with this idea:

I would love to see a switch statement we could use for objects, testing equality with each case: statement. @switch would work nicely as the name, and I envision it working like this:
@switch(myString) {
    case @"hello world":
        // Do something
        break;
    case @"another one":
        break;
}
This would be equivalent to writing the code using if statements and sending -isEqual: messages, but with a much more readable control flow.

The blocks approach

One suggested approach was to use a dictionary to map each "case" value to a block that should be executed when the "case" value matches the "switch" value. Jeff Biggus posted this solution mainly as an academic exercise, but I think it's about as tidy as the blocks-based approach gets:

typedef void (^voidBlock)(void);
 
#define swoosh( test_var__, action_dictionary__... ) \
        ((voidBlock)action_dictionary__[test_var__])();
 
[...]
 
NSString *test = @"that";
swoosh( test, @{
        @"this" : ^{ NSLog( @"found this" ); },
        @"that" : ^{ NSLog( @"found that" ); },
});

Nicolas Bouilleaud posted an alternative solution using +resolveInstanceMethod: that allows you to write:

[[@"foo" switch]
 case:@"bar" :^{ success = NO; }
 case:@"baz" :^{ success = NO; }
 case:@"foo" :^{ success = YES; }
 ];

I'm not crazy about using blocks for a few reasons:

  • You have to think about whether you need to use __block variables.
  • You can't put break, continue, or return statements in the blocks and have them work as they would in an analogous switch statement; often their very presence would be a syntax error.
  • If you use a dictionary of blocks, the "case" objects have to conform to NSCopying so that they can be dictionary keys. Also, as Nicolas points out, if you use a dictionary, you can't specify the order in which the cases are tested. So, no dictionaries; but you can imagine a similar approach using an array.
  • Xcode's auto-indenting of blocks looks really, really horrible. In theory, the typographical quirks of an IDE have nothing to do with the soundness of a technical approach, but Xcode is so bad about this that I can't ignore it. For example, if I put the first NSLog above on its own line, Xcode does this:
swoosh( test, @{
       @"this" : ^{
    NSLog( @"found this" );
},
       @"that" : ^{ NSLog( @"found that" ); },
       });

objswitch, objcase, endswitch

I don't know how original this is, but by using a few macros and a small class used behind the scenes, I came up with a different approach that is essentially syntactic sugar around nested else-if statements. Here's an example of how it looks, as formatted by Xcode:

objswitch(someObject)
objcase(@"one")
{
    // Nesting works.
    objswitch(@"b")
    objcase(@"a") printf("one/a");
    objcase(@"b") printf("one/b");
    endswitch
 
    // Any code can go here, including break/continue/return.
    // Xcode will indent it nicely.
}
objcase(@"two") printf("It's TWO.");  // Can omit braces.
objcase(@"three",  // Can have multiple values in one case.
        @"tres",
        @"trois") { printf("It's a THREE."); }
defaultcase printf("None of the above.");  // Default case is optional.
endswitch

I would argue this is even a tiny bit nicer than switch/case syntax, because you don't need unsightly break statements to keep the cases from bleeding into each other.

If someObject is @"one", the output is

oneb

If someObject is @"tres", the output is

It's a THREE.

In this example all the "case" values are strings, but they can be any object.

The "keywords" objswitch, objcase, objkind, and endswitch are actually macros. There is a simple class called ObjectMatcher that is instantiated by objswitch. You can see the code on GitHub.

objkind

Some people mentioned wanting similar syntax for testing the class of the object rather than its value. Testing an object's class is often a sign of suboptimal design (see here for a brief discussion, here for my thoughts on the matter), but for those occasions when you decide it's the right approach, you can use my objkind macro:

objswitch(someObject)
objkind(NSNumber) { printf("It's a NUMBER."); }
objkind(NSString) { printf("It's a STRING."); }
objkind([NSArray class],
        [NSDictionary class],
        [NSSet class]) printf("It's a collection.");
endswitch

Note that if you're only passing one class name to objkind, you can just give the class name:

objkind(NSNumber)

But if you pass multiple classes, you have to say [MyClass class] or [MyClass self] (or, if you prefer, MyClass.class or MyClass.self) for every item after the first one. This is a limitation of __VA_ARGS__ macros. I prefer all the items to look the same — hence:

objkind([NSArray class],
        [NSDictionary class],
        [NSSet class])

Like objcase, objkind is just a wrapper around a nested else-if statement, so you can freely mix objcase and objkind within the same objswitch.

selswitch, selcase

One place I might like a similar construct is for doing a switch statement on a selector. I have validateUserInterfaceItem: methods all over the place with lots of nested if-statements like this:

SEL itemAction = [anItem action];
 
if (itemAction == @selector(selectSuperclass:))
{
    // ...
}
else if (itemAction == @selector(selectAncestorClass:))
{
    // ...
}
else if ((itemAction == @selector(selectFormalProtocolsTopic:))
         || (itemAction == @selector(selectInformalProtocolsTopic:))
         || (itemAction == @selector(selectFunctionsTopic:)))
{
    // ...
}

I created selswitch and selcase macros that let you do this:

selswitch([anItem action])
selcase(@selector(selectSuperclass:))
{
    // ...
}
selcase(@selector(selectAncestorClass:))
{
    // ...
}
selcase(@selector(selectFormalProtocolsTopic:),
        @selector(selectInformalProtocolsTopic:),
        @selector(selectFunctionsTopic:))
{
    // ...
}
endswitch

I haven't decided whether the improvement is big enough that I'd use this rather than plain old nested ifs, especially since validateUserInterfaceItem: is called very frequently and should be as fast as possible.

[UPDATE: I've made several edits since I first published this post, ranging from fixing typos to inserting a sentence or two. I was originally flagging each change, but to reduce clutter I decided just to add this mention at the bottom.]