Enumerations in Objective-C
| January 6th, 2010I haven’t done much with DocBase since the last post, but I’ve by no means abandoned it. Right now, it’s serving well enough for development purposes in my application and I don’t want add features that end up being unneeded. However I did run into another issue that I thought was general enough to be talked about: enumerations.I’m not talking about collections, or objects that conform to the NSFastEnumeration protocol, but about the plain old C enums like this:
typedef enum {
Centimeters,
Meters,
Inches,
Feet
} Units;
Really, C enums aren’t a heck of a lot better than just declaring integer constants, other than the somewhat weak type safety they give you and a bit of self documentation (which in my mind is way more important than the type safety). Other languages offer a little more power in their enumeration constructs. So here’s a few of the things I want:
- Take values out of the global namespace
- Easy conversion to and from strings for serialization
- Extensibility for value-specific behavior
The first 2 points are probably pretty self-explanatory. The third might need a bit of explanation. Say we wanted a method to convert a value and units into miles. We’d have to write a function with a switch statement that converted each of the different unit types into miles. I hate switch statements. Aside from my own personal hatred, there are possibly better ways we can handle this.
Fortunately, Objective-C offers us some great ways to handle all these problems. First lets outline what I want to end up with.
@interface BREnumType : NSObject {
NSString* _name;
}
-(id)initWithName:(NSString*)name;
-(NSString*)stringValue;
@end
This is our basic enumeration value. It’s pretty simple: the stringValue method returns it’s name, and I don’t need to override any methods like isEqual: or hash since I plan on each value being a singleton. Next for the actual enumeration I want it to look something like this:
@interface BRUnits : BREnumType {
}
+(BRUnits*)valueWithString:(NSString*)s;
+(BRUnits*)centimeters;
+(BRUnits*)meters;
+(BRUnits*)inches;
+(BRUnits*)feet;
@end
Again, pretty straightforward. The only thing that might be of slight interest is the implementation of valueWithString:. In my first implementation I was going with a dictionary of all the values in the enumeration keyed by their string value. This has some problems with the timing of when that dictionary is initialized. Fortunately that led me to a much better implementation:
+(BRUnits*)valueWithString:(NSString*)s
{
SEL selector = NSSelectorFromString(s);
if ([self respondsToSelector:selector]) {
return [self performSelector:NSSelectorFromString(s)];
}
return nil;
}
With the exception of the return type, this method could actually be pushed down into BREnumType. I chose to keep it in BRUnits just to keep the return type.
I want each different value in the enumeration to have it’s own class. Why? Because I want to be able to add functionality specific to each value in the enumeration with categories. So we’ll actually end up with:
@interface BRUnits_centimeters : BRUnits {
}
@end
and so on for each different value. Then the implementation for the centimeters method would look something like:
static BRUnits* _BRUnits_centimeters_value = nil;
+(BRUnits*)centimeters
{
if (_BRUnits_centimeters_value == nil) {
_BRUnits_centimeters_value = [[BRUnits_centimeters alloc] initWithName:@"centimeters"];
}
return _BRUnits_centimeters_value;
}
Some would probably point out that I’m not doing enough to protect the singleton nature of each of the value classes. That’s true, I’m not, and I’m not really concerned about it. Since I’m the consumer, I’m pretty sure I won’t do silly things like create an instance of BRUnits_centimeters. If it turns out to be a problem, I’ll revisit it. However, another problem is that it isn’t thread-safe. While it’s not going to cause me an issue right now, it’s something I’ll definitely correct at some point.
Now, using categories we can add value specific methods to each class.
@interface BRUnits(Conversion)
-(double)convertToMiles:(double)value;
@end
@implementation BRUnits_centimeters(Conversion)
-(double)convertToMiles:(double)value {
return value / (2.54 * 12 * 3 * 1760);
}
@end
We can put default behavior in on BRUnits or simply ignore it (the linker doesn’t seem to complain). So I’ve gained all the 3 benefits I was looking for, but there’s one more issue: this is a lot of boilerplate code that I don’t want to write every time I want an enumeration.
So we fall back on a familiar tool: the preprocessor. Many people would say that using the preprocessor to generate code is evil. Sometimes I agree, but I just want what’s easy. Using the preprocessor can lead to hard to understand compiler errors, and it doesn’t always yield the best syntax, but this is small enough that I feel it’s way better than writing some separate tool to generate all this code. Another option might be to generate code at runtime. I didn’t really investigate this too hard, so I can’t really say that it’s better or worse than using macros. Also, I’m willing to admit that my macro-fu is weak compared to many, there might be better ways to write the macros than what I came up with.
So what does it boil down to? We still end up needing interface and implementation files for our enumeration which is one weakness over straight C enums, but otherwise it’s not too bad. In the interface we have:
BR_ENUM(BRUnits)
BR_ENUM_VALUE(BRUnits, centimeters)
BR_ENUM_VALUE(BRUnits, meters)
BR_ENUM_VALUE(BRUnits, inches)
BR_ENUM_VALUE(BRUnits, feet)
And in the implementation, almost the exact same thing:
BR_ENUM_IMP(BRUnits)
BR_ENUM_VALUE_IMP(BRUnits, centimeters)
BR_ENUM_VALUE_IMP(BRUnits, meters)
BR_ENUM_VALUE_IMP(BRUnits, inches)
BR_ENUM_VALUE_IMP(BRUnits, feet)
I won’t go too much into the details of the macros themselves. The code they generate closely follows the code I first wrote with one big exception: each of the methods on the BRUnits class to get back a specific value went into it’s own category on BRUnits. This was done to avoid having to list out the values more than the 2 times I already have.
I don’t really consider this a replacement for C enums. They are still fine for many things and are certainly simpler and faster if all you need is the constant value. But there are often cases were I want a bit more functionality around an enumeration and this seems to do well enough for me. To use it, you’d only need to include the EnumType.h and EnumType.m files in your project and you’re good to go.