Cocoa for Scientists (Part XI): The Value in Keys

·

·

In the coming tutorials, I plan to introduce Cocoa Bindings and contrast it with the ‘traditional’ outlet/action approach to app development. But in order to understand how Bindings work, you first need to know about the underlying Cocoa technologies, namely, Key-Value Coding (KVC) and Key-Value Observing (KVO). In this short tutorial, I will introduce you to the marvels of keys and key paths, which underpin much of the Cocoa coolness. Next time, I will take a look at Key-Value Observing.

Key-Value Coding

Key-Value Coding (KVC) is actually not a new technology at all — it was there long before Mac OS X was even a twinkle in Steve Jobs’ eye. KVC leverages the dynamicism of Objective-C to allow you to access the properties of an object using strings to identify them. Usually when you want to retrieve a property, you would call a getter accessor, like this

float height = [object height];

This ‘hard-coded’ call is not that flexible. For example, what you if you don’t know which method you want to call at compile time? Maybe you determine the method from some runtime data.

KVC allows you to get or set a property with only a string for identification. For example, the invocation above could be replaced by this:

NSNumber *number = [object valueForKey:@"height"];
float height = [number floatValue];

or more concisely

float height = [[object valueForKey:@"height"] floatValue];

This may seem like a step back in terms of legibility, but it is a big step forward in flexibility, and many of Cocoa’s coolest features are only made possible by this flexibility.

So what is happening when you invoke the valueForKey: method (which, incidentally, derives from the NSObject class)? The Objective-C runtime goes through the methods that belong to the object in question, and looks for a getter with the name height or getHeight. If it finds one, it invokes it, returning the result. If the result is a simple type, like a float or int, it first converts it into an NSNumber object before returning it.

What happens if there is no method called height? Then the runtime will look at the instance variables belonging to the object, and see if one exists called height or _height. If it finds one, it will return its value directly.

There are actually strict rules about how the variables and methods can be named, and the order in which they are sought, but you don’t really need to know them unless you want to get funky with variable names (…and you shouldn’t get funky with variable names).

The flip side of getting a value is setting a value. The KVC setter is called setValue:forKey:. It works the same as for getters, but first tries to call a set...: method, before setting the value directly. Here is an example of setting the height attribute:

[object setValue:[NSNumber numberWithFloat:180.0] forKey:@"height"];

This will look for a method called setHeight: and invoke it if found. KVC is smart enough to see that setHeight: expects to get a float argument, and that the NSNumber you have passed to setValue:forKey: needs to be transformed before being passed in. If there is no setHeight: method, the runtime system will proceed to look for a height or _height instance variable (ivar), and set that directly. If you are going to let KVC set variables directly, bear in mind that it will not do any memory management for you; if you need memory management for a particular attribute, make sure you define the appropriate accessor methods.

Key Paths

KVC starts to get much more interesting when you learn that it isn’t restricted to simple keys, but can take key paths. A key path is a bit like a path in the file system, but for objects rather than files and directories. The directory path /Users/cormack/Library/Application Support gives the location of my Application Support directory relative to the root of the file system. The same trick works with KVC:

int numAtoms = [[chemicalSystem valueForKeyPath:@"molecule.numberOfAtoms"] intValue];

The invocation of valueForKeyPath: looks for an object at the path molecule.numberOfAtoms relative to the ‘root’ object chemicalSystem. The path delimiter is a point, rather than a forward slash, but the concept is the same.

So what will this code do? The Objective-C runtime will first use KVC to retrieve an object for the attribute molecule. It will use the same rules discussed earlier, first looking to see if there is an accessor method (eg molecule or getMolecule), and if not trying direct access to the ivar. When it has retrieved the object corresponding to the molecule, it will use KVC to retrieve the numberOfAtoms attribute of that object. This NSNumber object is what gets returned from valueForKeyPath:.

Not surprisingly, the same trick can be used for setting an attribute:

[chemicalSystem setValue:[NSNumber numberWithInt:10] forKeyPath:@"molecule.numberOfAtoms"];

Before moving on, it is also interesting to note that KVC is a form of intelligent high-level messaging. We have already seen that it knows how to change the arguments and return values of functions, and that it can follow a path through an object graph, but it can do even more. For example, you can retrieve whole arrays of objects:

NSArray *atomicMasses = [chemicalSystem valueForKeyPath:@"molecule.atoms.mass"];

This assumes the following class interfaces:

@interface ChemicalSystem : NSObject {
    Molecule *molecule;
}
-(Molecule *)molecule;
@end

@interface Molecule : NSObject {
    NSArray *atoms;
}
-(NSArray *)atoms;
@end

@interface Atom : NSObject {
    float mass;
}
-(float)mass;
@end

KVC is smart enough to see that the atoms attribute in the Molecule class is an array, and it will loop over the array extracting the mass for each object in it. The NSArray returned will contain NSNumbers representing the atomic masses of each of the atoms in the chemical system. Without KVC, you would need to write a loop and many more lines of code:

NSMutableArray *atomicMasses = [NSMutableArray array];
Molecule *molecule = [chemicalSystem molecule];
NSArray *atoms = [molecule atoms];
int i;
for ( i = 0; i < [atoms count]; ++i ) {
    Atom *atom = [atoms objectAtIndex:i];
    [atomicMasses addObject:[NSNumber numberWithFloat:[atom mass]]];
}

Cocoa is All About Conventions

The more you work with Cocoa, the more you realize that it is a convention-based system. Traditionally, programming languages and APIs have been designed to force you to declare things up front. If you are going to call a method at some point in your code, the compiler needs to know what method that is, or at least that it exists. This approach could be summarized as the ‘configuration’ approach to development. You have to configure everything explicitly before your software runs. By its nature, it is an inflexible approach.

The trend lately — in new web frameworks like Ruby On Rails, for example — is to favor a convention-based approach. Rather than requiring the developer to explicitly define all relationships in the software, newer frameworks and languages tend to define implicit conventions. This can dramatically reduce the amount of work you have to do, and the resulting software is often much more amenable to change.

Cocoa could be considered one of the ancestors of these cool new languages/APIs — KVC is a convention-based approach to development. By sticking to the Cocoa accessor naming conventions, and thereby allowing relationships to be defined implicitly, you are saving yourself a lot of work, and ultimately the software is more supple to change.

Next Stop: KVO

Next time we will take a look at the flip size of KVC: Key-Value Observing (KVO). This is used not to get and set attributes, but to observe when they have changed. After that, we will be ready to step into Cocoa Bindings, which allows you to connect up your user interface to your model data using the key paths that I started introducing today.


Leave a Reply

Your email address will not be published. Required fields are marked *