« Understanding the design behind Cocoa's Foundation classes
by H. Lally Singh
(website | email)There's that initial moment when you first look at a large library like Foundation and say to yourself, "Gee, that's a lot of classes. Where do I start?" This article should help you get familiar with some of the ideas behind Foundation.
// Mutable?
Mutability isn't something most programmers normally worry about, but for optimization reasons, it's important. Suppose I have a list of items, but I know that the list is never going to change. I can just use a simple array and never have to worry about those items ever again. However, if I may add or remove items from the list, I've got to get smarter about my container. This means I probably have to implement a linked list.
So, that settles it, I'll write a really good smart container once and forget about it.
No! Just because I sometimes need the ability to add or remove items from a list doesn't mean I should always take a memory and performance hit. In order to minimize the costs of using a linked list versus an array, I'm best off with two containers: one optimized for modification, and one optimized for never having to change. These containers are mutable and immutable, respectively.
With Cocoa, we get both. Cocoa gives us the simple immutable containers like NSArray and NSDictionary, and their mutable counterparts NSMutableArray and NSMutableDictionary. Ok, now you know which one to use in your code, but what happens when you have an immutable NSArray and want an NSMutableArray? Try this:
NSArray * foo = SomeFunctionReturningAnNSArray(); NSMutableArray * bar = [foo mutableCopy]; [foo release];
Simple enough. How about the other way? All the NSMutable* classes inherit from their immutable counterparts, so no conversion is necessary! So, switching the types from above and trying again, we end up with much simpler code:
// Quickly converting from a mutable container to its immutable counterpart.
NSMutableArray *foo = SomeFunctionReturningAnNSMutableArray();
NSArray * bar = foo;
In the above case, bar will still actually point to an NSMutableArray, it just so happens that NSMutableArray can do everything an NSArray can, so there's no problem. But, say you no longer need the ability to add and remove elements from your container, and want to strip off that overhead? The best way is to make an immutable copy of the original, and then release that original:
// Stripping off the additional functionality of a mutable container for // efficiency. NSMutableArray *foo = SomeFunctionReturningAnNSMutableArray(); NSArray * bar = [foo copy]; [foo release];
So, if we're trying to be more efficient, why do we make a copy? Firstly, remember that the container only stores references to objects, so the copy operation never copies the actual objects, just references to them. Secondly, it's much faster to access items from an array than from a linked list. Finally, the resultant NSArray will take up less memory than the NSMutableArray just released.
Simple! Easy! Cocoa!
// Which Container?
There are plenty of containers in Cocoa: NSArray, NSMutableDictionary, NSMutableSet, NSData, and many others. Which one to use? We've already covered whether or not you want NSMutableBlah or NSBlah. But which Blah? Choosing the right container depends on the answers to two questions you have to ask yourself:
- What am I trying to store?
- How do I want to access it?
If you're trying to store a sequence of individual characters, the NSString family (NSString, NSMutableString) is your best choice. If you want to also store attributes about the characters, like size, weight, typeface and color, the NSAttributedString family is probably best. But, if you're thinking of sequences of characters, you're probably already thinking "string."
For a sequence of bytes with no other meaning to you right now, the NSData family is your choice. This is useful for binary blobs that you just have to transport to something else, like an MPEG decoder or a disk burner subroutine.
Now we get to the interesting part. For any other type of object, you've got the more generalized containers to work with: NSArray, NSDictionary, and NSSet. At this point, your decision rests with the second question from above.
If you want an ordered sequence of objects, the NSArray family is your choice. You can access each element with an integral index, just like a normal C array. NSArray is a very popular choice in modern applications, all the latest apps are using it, so should you :-)
If an integral index isn't the most natural way to refer to an object for your application, then you may want to consider the NSDictionary family. The NSDictionary family refers to one object using another object called a key. The most common case is with an NSString acting as the key. Then, you can associate objects with human-readable strings. Using this functionality, bundled with the ability for NSDictionary to read and write itself from and to a file make it very handy for very, very easy configuration files.
And finally, if you just want a set of objects in the mathematical sense, then the NSSet family is your best bet. It's optimized for testing whether or not an object is in it. Also, it has methods to create unions and intersections of sets, and check if one set is a proper subset of the other.
Well, there you have it, a container selection guide for Cocoa's Foundation classes. As usual, Apple's documentation on this stuff is really good. Here's a link for their data management classes in general, and here's a link for their collection classes (I've call them containers because I'm a freak).
// Enumerators
Going through all the elements of an NSArray is pretty simple, just ask it for the 0th element, and then the 1st, and so on until you hit [foo count]-1. But, what about the other containers? For everything else, Cocoa provides you with Enumerators. Enumerators are simply objects that let you access every element in a container, one at a time. For example, to print out all the keys in an NSDictionary:
NSDictionary * foo = SomeFunctionReturningAnNSDictionary(); NSEnumerator * e = [foo keyEnumerator]; id obj; while (obj = [e nextObject]) { NSLog(@"Key: %@", obj); }
NSDictionary also provides an objectEnumerator method so you can access all the contained objects without constantly calling objectForKey: for every enumerated key.
NSEnumerator is an extremely simple class. The only two methods in it are nextObject, which you've already seen, and allObjects, which returns an NSArray of all the objects it enumerates over.
NSSet also provides an enumerator over its contained objects via its objectEnumerator method.
// Strings
Strings are so important to programming that we always have to consider them separately from everything else. We use them in many more ways than any other container. So, we'll cover which string class to use, and how to use them well.
NSString is immutable, as it should be. It also has many, many methods. We'll cover some of the most used here.
- {int,float,double}Value - intValue, floatValue, and doubleValue all try and read a numeric value from the text in the string and return it if they were successful, 0 otherwise. These are the equivalents of atoi() and strtod() in C.
- stringByExpandingTildeInPath: - this method returns a copy of the string with the tilde (~) expanded to the user's home directory. The tilde is a very nice convention coming in from Mac OS X's UNIX roots. For example, the string "~/Desktop" on my computer would expand to "/Users/lally/Desktop" after calling this method.
- stringByAppendingString: - the Cocoa equivalent of strcat(), without all the security holes.
- stringByAppendingFormat: - my personal
favorite. This method takes a printf()-style format
specification and a variable number of arguments and does what would take an
sprintf() and a strcat() in
C to do. Here's an example:
NSString * s = @"Foo"; NSLog("%@\n", [s stringByAppendingFormat:@"3=%d",3]);
- stringWithCString, cString - these methods convert from and to a C-style NULL-terminated string.
NSString also has two nice little features I'd like to cover before letting you go. Firstly, NSString has a sibling class called NSAttributedString, which handles formatted text (bold, italics, typefaces, etc.). This is how you write your own TextEdit :-).
Finally, NSObject contains a method called description, which returns an NSString. This is one very handy way to print out debugging information. Simply pass a "%@" into NSLog or stringByAppendingFormat:, and that parameter will have its description method called on it. Very handy. Very, very handy.
// Conclusion
Cocoa's Foundation classes provide a very powerful API, but they're also highly optimized. For that reason, we have to worry a bit about some less than obvious details like whether or not we intend to modify the contents of a container, or how we plan to access a container. Luckily, once we've decided the path to take, Foundation gives us a powerful API for leveraging our choice.