Typesmart Objective C Enums

Object Oriented Enums
Being built on C, Objective-C programs use traditional C style enums for a variety of purposes. However, there are a few problems with this approach:

  • They aren’t objects, so if you want to, for example, use them as a key in a dictionary, you have to wrap them up in an NSNumber instance
  • They aren’t extensible (without sacrificing type safety). You can’t have one enum inherit from another.
  • They are fragile - add a new enum anywhere other than at the end of the list, and watch all your old documents fail
  • They are difficult to log - while debuggers usually have enough symbol information to show symbolic enum values (assuming you have them stored in an enum typed variable, as opposed to a plain int), there’s no easy way to print that symbolic value.
  • They all live in the same namespace, so you can’t have two enum named “ON”, resulting in enum values that have long unique type information prefixing them.
One common approach that deals with some of these problems is to use constant NSString *. This solves several problems:
  • They are objects, so using them as keys in a dictionary is trivial
  • They aren’t fragile (so long as you don’t change their actual internal representation).
  • Trivial to log, since their value is a string that is (normally) a unique and descriptive value.
Unfortunately, you loose a few important features:
  • No longer type-safe - all constant strings are the same type.
  • Comparing can’t be done with a simple “==” test - an “isEqual:” message must be sent, slowing down the program, and making the source code wordier.
  • Still have namespace issues.
This paper presents a solution that:
  • Is an object
  • Type safe and flexible (but not truly extensible via subclassing)
  • Non fragile
  • Trivial to log
  • Can be compared using “==
  • Provide namespace support
The solution takes advantage of the (sometimes controversial) “dot notation” in Objective-C 2.0.
What it looks like
Below are some samples of both the declaration of an object oriented enum, and how it can be used:
@interface Color : GandEnum
  • (Color *)RED;
  • (Color *)GREEN;
  • (Color *)BLUE;
@end

...
Color *someColor = Color.RED;
if (someColor != Color.GREEN) {
NSLog(@”The color is %@ instead of %@”,
someColor, Color.GREEN);
}
[colorList addObject: someColor];
colorName.text = someColor.name;

As you can see, enums are declared as classes, a subclass of GandEnum, a class that provides some infrastructure required to meet some of the above goals. Each enum value is then a different class method, returning an instance of that enum class - this gives us type safety, extensibility, and namespace support (since multiple classes can have the same method names, but in our case the class and receiver will be fully typed so there won’t be a problem with potential method name collisions). One could try to make a subclass of the enum class (but that unfortunately won’t work like one would hope - see below). However, one can use a category to add additional enum values. By using the Objective-C 2.0 dot notation, we also keep the code clean.
By making this a class, we also get easy introspection - the
GandEnum superclass contains both the name as well as an ordinal value for easy logging. This also gives us a non-fragile storage solution: we save the name and ordinal value, and upon unarchiving, the name wins (unless it not longer exists, then we make due with old name and ordinal value, which handles the case of renaming the enum). By keeping an ordinal value, we can do ordinal arithmetic.
One can also add additional instance variables in subclasses, allowing you to have additional information associated with the enum - this blurs the boundary between an enum and regular data objects a bit, but can come in handy (for example, the color enums may contain corresponding rgb values).
One critical point to remember is that enum instances are all singletons, by design. This allows us to use the “
==” operator to test equality instead of having to send a message, as well as prevent a proliferation of objects.
There are, however, some drawback:
  • Since they are values, and not compile time constants, they can’t be used as values in a switch statement. Through the use of blocks, one could easily imagine a purely object oriented, Smalltalk style, control structure built upon messages, such as:
[someColor switchCase:
Color.RED,
^{ [[UIColor redColor] set]; },
Color.GREEN,
^{ [[UIColor greenColor] set]; },
nil];
  • Again, since they aren’t constants, it is not possible to declare static arrays of that length:
double Hues[Color.LAST]; // doesn’t work
// workaround
double *Hues;
...
Hues = calloc(Color.LAST, sizeof(double));
A note on using UPPERCASE
Enums are declared entirely in uppercase for several reason. While normally methods should not be declared thusly, these are designed to be special constants, more akin to the way that a #define macro constant would be in all uppercase. Stylistic matters aside, this convention is used when determining the list of all enumeration for a class (one can’t just look at the class method list and assume everything is an enum singleton).
Why Subclassing an enum class doesn’t work
It seems tempting to subclass one enum class from another:
@class ProcessColor : Color
  • (ProcessColor *) LOGORED;
  • (ProcessColor *) CORPORATEBLUE;
...
Unfortunately, that isn’t going to work as expected, for several reasons. First, creation of these enum instances is lazy, and is done by allocating an instance of the receiver class. So these two fragments would have different results depending on their order of execution:
NSLog(@”red %@”, Color.RED);
NSLog(@”red %@”, ProcessColor.RED);
...
NSLog(@”red %@”, ProcessColor.RED);
NSLog(@”red %@”, Color.RED);
Assuming that neither enum has been used before, the first will result in ProcessColor.RED being an instance of Color while the second results in Color.RED being an instance of ProcessColor.RED. As a result, trying to get the previous (wrap around) enum of Color.RED will either be Color.BLUE or ProcessColor.CORPORATEBLUE.
Worse, if the subclass declares additional properties, they may no exists in
ProcessColor.RED (since it is actually Color.RED).
So it looks good, and almost works, but doesn’t.
Other Goals
Besides the above mentioned goals, we want our enum classes to support other useful functionality:
  • Convert from NSString * name to the enum
  • Convert from NSInteger ordinal value to the enum
  • Be able to get an NSArray * of all the enums in that class
  • Find the first, last enum (trivial with the array above)
  • Get the next and previous enum, or nil if none (as well as a “wrap around” version of these)
  • Storing additional information with the enum (which can easily be done via subclasses, but this requires additional work in the class implementation to properly initialize it - a general purpose way would be nice).
Header File
Available as http://dev.gandreas.com/ooenum/GandEnum.h
//
// GandEnum.h
//
// Created by glenn andreas on 2/17/10.
// Copyright 2010 glenn andreas. .
//
//All rights reserved.
//
//Redistribution and use in source and binary forms, with or without
//modification, are permitted provided that the following conditions are met:
//* Redistributions of source code must retain the above copyright
//notice, this list of conditions and the following disclaimer.
//* Redistributions in binary form must reproduce the above copyright
//notice, this list of conditions and the following disclaimer in the
//documentation and/or other materials provided with the distribution.
//* Neither the name of the nor the
//names of its contributors may be used to endorse or promote products
//derived from this software without specific prior written permission.
//
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
//DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#import

@interface GandEnum : NSObject {
NSString *name;
int ordinal;
NSDictionary *properties;
// cached to speed up prev/next - these are all "assign", not that it matters because they are all singletons
id previousWrappingEnum;
id nextWrappingEnum;
BOOL isFirstEnum, isLastEnum, isCacheValid;
}
@property (nonatomic, retain) NSString *name;
@property (nonatomic, assign) int ordinal;
+ (
id) enumFromName: (NSString *) name;
+ (
id) enumFromOrdinal: (int) ordinal;
+ (
NSArray *) allEnums;
// note the use of id make these no longer type safe
+ (
id) firstEnum;
+ (
id) lastEnum;
@property (nonatomic, readonly) id previousEnum;
@property (nonatomic, readonly) id nextEnum;
@property (nonatomic, readonly) id previousWrappingEnum;
@property (nonatomic, readonly) id nextWrappingEnum;
- (
id) deltaEnum: (NSInteger) delta wrapping: (BOOL) wrapping;
// this should only be called from with the enum declaration methods
- (
id) initWithName: (NSString *) name ordinal: (int) ordinal properties: (NSDictionary *) properties;
+ (
void) invalidateEnumCache; // if you've done dynamic code loading and added an enum through a category, call this for each enum class modified
@end

// Macro to declare the enum implementation. The name and ordinal value are the first two parameters, what follows is then a list of objects and keys for additional properties
// This list is used for +[NSDictionary dictionaryWithObjectsAndKeys:] - note that you do not need to specify a nil at the end (it is added automatically)
// These properties will be dynamically generated. Currently, properties are restricted to objects, classes, SEL (encoded as strings), and scalar values.
// Generic pointers (include c-strings) should be enocded via [NSValue valueWithPointer: ptr]
// Under UIKit, we also support CGPoint, CGSize, and CGRect (encoded as NSValue *)
// Under AppKit, we also support NSPoint, NSSize and NSRect (encoded as NSValue *)
// For now, encode all other structs as NSValues and make the property be of type NSValue *, or explicitly implement the property yourself
#define GANDENUM(ename, evalue, eproperties...) \
+ (id) ename { \
static id retval = nil; \
if (retval == nil) { \
retval = [[self alloc] initWithName: @ #ename ordinal: evalue properties: [NSDictionary dictionaryWithObjectsAndKeys: eproperties, nil]]; \
}\
return retval;\
}

#if 0
/* So, for example: */
@interface Color : GandEnum
+ (Color *) RED;
+ (Color *) GREEN;
+ (Color *) BLUE;
@property (nonatomic, readonly) float hue;
@end

// ...


@implementation Color
@dynamic hue;
GANDENUM(RED,
0, [NSNumber numberWithFloat: 0.0], @"hue")
GANDENUM(GREEN,
1, [NSNumber numberWithFloat: 1./3.], @"hue")
GANDENUM(BLUE,
2, [NSNumber numberWithFloat: 2./3.], @"hue")
@end

void test()
{
for (Color *c = Color.firstEnum; c != nil; c = c.nextEnum) {
NSLog(@"Color: %@", c);
if (c == Color.GREEN) {
NSLog(@"This is green, hue = %g", c.hue);
}
}
}
#endif

Implementation File
Available as http://dev.gandreas.com/ooenum/GandEnum.m
To Do
  • Thread Safety