Continuing a theme of tearing out useful bits of functionality from Oolite, and inspired by recent Twitter exchanges with Craig Hockenberry and Rainer Brockerhoff, here’s some really boring code for you all – gruntwork that you don’t want to repeat, but should probably be using.
Executive summary for people who don’t like rambling anecdotes: property lists and user defaults should always be type-checked, and this makes it easy.
Oolite is highly moddable, with over 150500 expansion packs. Most of them are interoperable, and typically many will be in use at once. Essentially all of these contain property lists, generally written by hand by non-programmers, often on systems without plutil or Property List Editor. As such, many contain parse errors or structural problems.
Previous versions of Oolite blithely ignored the imperfections of mere humans. Unparseable property lists were effectively ignored, leading to unexpected behaviour, often quite subtle. Structural errors, however, caused exceptions, most of them seemingly at random.
The current test releases address the syntax issues by dumping the error string from NSPropertyListSerialization
and the path to the relevant file to the game’s run log. A certain amount of context-sensitive type checking is done in certain areas, such as scripting (yes, property lists are used for scripting, although that’ll be deprecated in the next stable release).
The most significant fix, however, has been to replace most uses of -objectForKey:
and -objectAtIndex:
with methods that check types and perform conversions where they make sense. In particular, strings are automatically converted to numbers and booleans where appropriate, which is needed to work with the more pleasant OpenStep plist syntax. When a plist value can’t be converted to the required type, or simply isn’t there, an appropriate default value is used. The game will no longer try to get keyed objects from an array, or find the first character of a dictionary. Instead of code like this:
// Hope plist has correct structure.
unsigned myMagicNumber = [[[plist objectForKey:@"someArray"] objectAtIndex:3] unsignedIntValue];
or more fault-tolerant code like this:
unsigned myMagicNumber = 0;
id array = [plist objectForKey:@"someArray"];
if ([array isKindOfClass:[NSArray class]] && [array count] > 3)
{
id number = [array objectAtIndex:3];
if ([number respondsToSelector:@selector(unsignedIntValue)])
{
myMagicNumber = [number unsignedIntValue];
}
}
we now have code like this:
unsigned myMagicNumber = [[plist arrayForKey:@"someArray"] unsignedIntAtIndex:3];
which will not throw an exception if @"someArray"
is not an array or if the array has less than four members, but will instead return 0. It’s also shorter than the broken version. Default values other than 0 can be provided in the form [array integerAtIndex:3 defaultValue:-1]
or [dictionary unsignedShortForKey:@"value" defaultValue:14]
.
Oolite is obviously an extreme case, but many applications deal with property lists. Even more deal with NSUserDefaults
, which of course is a wrapper around property lists. You may think that you know what you’re putting into user defaults and can therefore rely on it to be correctly structured, but if you’re thinking that you’re wrong. First, users will modify your preferences file, break it, and forget they did it. Second, your code is buggy. If you write any sort of collection to your prefs, there’s a chance that you’ll put an object of the wrong class there if the phase of the moon is unusual. If you don’t check it when you read it, this will cause an exception, and you’ll end up guiding a user through digging around in ~/Library to clean up your mess. Better to be fault-tolerant to start with.
So, here it is: JAPropertyListAccessors 1.2 (MIT/X11 license). It provides:
- A non-throwing variant of
-[NSArray objectAtIndex:]
.
- Functions to convert objects (NSNumber or NSString) to all standard integer types, as well as float and double, clamping the results rather than overflowing, and returning a user-specified default value if no conversion can be performed.
- Convenience methods for accessing integers, floats, strings, arrays, dictionaries, sets, or objects of a specified class from
NSArray
, NSDictionary
or NSUserDefaults
. The following transparent conversions are performed when appropriate: NSString
to numbers; NSNumber
and NSDate
to NSString
; NSArray
to NSSet
.
- Convenience methods to set integers and floating-point values in
NSMutableArray
, NSMutableDictionary
and NSUserDefaults
.
- Unit tests for the primitive conversion functions.
- Compatible with 32-bit and 64-bit runtimes, and with garbage-collected and non-GC code.
- Very little in the way of comments.
Update (version 1.0.1): Default value (rather than nil) is now returned when conversion to string fails. String conversion now uses -stringValue
if available.