title | author | category | tags | excerpt | status | ||||
---|---|---|---|---|---|---|---|---|---|
KVC Collection Operators |
Mattt |
Cocoa |
nshipster |
Rubyists laugh at Objective-C’s bloated syntax. Although we lost a few pounds over the summer with our sleek new object literals, those Red-headed bullies still taunt us with their map one-liners and their fancy Symbol#to_proc. Fortunately, Key-Value Coding has an ace up its sleeves. |
|
Rubyists laugh at Objective-C's bloated syntax.
Although we lost a few pounds over the summer with our sleek new object literals, those Red-headed bullies still taunt us with their map
one-liners and their fancy Symbol#to_proc
.
Really, a lot of how elegant (or clever) a language is comes down to how well it avoids loops. for
, while
; even fast enumeration expressions are a drag. No matter how you sugar-coat them, loops will be a block of code that does something that is much simpler to describe in natural language.
"get me the average salary of all of the employees in this array", versus...
double totalSalary = 0.0;
for (Employee *employee in employees) {
totalSalary += [employee.salary doubleValue];
}
double averageSalary = totalSalary / [employees count];
Meh.
Fortunately, Key-Value Coding gives us a much more concise--almost Ruby-like--way to do this:
[employees valueForKeyPath:@"@avg.salary"];
KVC Collection Operators allows actions to be performed on a collection using key path notation in valueForKeyPath:
. Any time you see @
in a key path, it denotes a particular aggregate function whose result can be returned or chained, just like any other key path.
Collection Operators fall into one of three different categories, according to the kind of value they return:
- Simple Collection Operators return strings, numbers, or dates, depending on the operator.
- Object Operators return an array.
- Array and Set Operators return an array or set, depending on the operator.
The best way to understand how these work is to see them in action. Consider a Product
class, and a products
array with the following data:
@interface Product : NSObject
@property NSString *name;
@property double price;
@property NSDate *launchedOn;
@end
Key-Value Coding automatically boxes and un-boxes scalars into
NSNumber
orNSValue
as necessary to make everything work.
Name | Price | Launch Date |
---|---|---|
iPhone 5 | $199 | September 21, 2012 |
iPad Mini | $329 | November 2, 2012 |
MacBook Pro | $1699 | June 11, 2012 |
iMac | $1299 | November 2, 2012 |
@count
: Returns the number of objects in the collection as anNSNumber
.@sum
: Converts each object in the collection to adouble
, computes the sum, and returns the sum as anNSNumber
.@avg
: Takes thedouble
value of each object in the collection, and returns the average value as anNSNumber
.@max
: Determines the maximum value usingcompare:
. Objects must support comparison with one another for this to work.@min
: Same as@max
, but returns the minimum value in the collection.
Example:
[products valueForKeyPath:@"@count"]; // 4
[products valueForKeyPath:@"@sum.price"]; // 3526.00
[products valueForKeyPath:@"@avg.price"]; // 881.50
[products valueForKeyPath:@"@max.price"]; // 1699.00
[products valueForKeyPath:@"@min.launchedOn"]; // June 11, 2012
{% info %}
To get the aggregate value of an array or set of NSNumber
values,
you can simply pass self
as the key path after the operator.
For example,
[@[@(1), @(2), @(3)] valueForKeyPath:@"@max.self"]
produces the value 3
.
{% endinfo %}
Let's say we have an inventory
array, representing the current stock of our local Apple store (which is running low on iPad Mini, and doesn't have the new iMac, which hasn't shipped yet):
NSArray *inventory = @[iPhone5, iPhone5, iPhone5, iPadMini, macBookPro, macBookPro];
@unionOfObjects
/@distinctUnionOfObjects
: Returns an array of the objects in the property specified in the key path to the right of the operator.@distinctUnionOfObjects
removes duplicates, whereas@unionOfObjects
does not.
Example:
[inventory valueForKeyPath:@"@unionOfObjects.name"]; // "iPhone 5", "iPhone 5", "iPhone 5", "iPad Mini", "MacBook Pro", "MacBook Pro"
[inventory valueForKeyPath:@"@distinctUnionOfObjects.name"]; // "iPhone 5", "iPad Mini", "MacBook Pro"
Array and Set Operators are similar to Object Operators, but they work on collections of NSArray
and NSSet
.
This would be useful if we were to, for example, compare the inventory of several stores, say appleStoreInventory
, (same as in the previous example) and verizonStoreInventory
(which sells iPhone 5 and iPad Mini, and has both in stock).
@distinctUnionOfArrays
/@unionOfArrays
: Returns an array containing the combined values of each array in the collection, as specified by the key path to the right of the operator. As you'd expect, thedistinct
version removes duplicate values.@distinctUnionOfSets
: Similar to@distinctUnionOfArrays
, but it expects anNSSet
containingNSSet
objects, and returns anNSSet
. Because sets can't contain duplicate values anyway, there is only thedistinct
operator.
Example:
[@[appleStoreInventory, verizonStoreInventory] valueForKeyPath:@"@distinctUnionOfArrays.name"]; // "iPhone 5", "iPad Mini", "MacBook Pro"
Curiously, Apple's documentation on KVC collection operators goes out of its way to make the following point:
Note: It is not currently possible to define your own collection operators.
This makes sense to spell out, since that's what most people are thinking about once they see collection operators for the first time.
However, as it turns out, it is actually possible, with a little help from our friend, objc/runtime
.
Guy English has a pretty amazing post wherein he swizzles valueForKeyPath:
to parse a custom-defined DSL, which extends the existing offerings to interesting effect:
NSArray *names = [allEmployees valueForKeyPath: @"[collect].{daysOff<10}.name"];
This code would get the names of anyone who has taken fewer than 10 days off (to remind them to take a vacation, no doubt!).
Or, taken to a ridiculous extreme:
NSArray *albumCovers = [records valueForKeyPath:@"[collect].{artist like 'Bon Iver'}.<NSUnarchiveFromDataTransformerName>.albumCoverImageData"];
Eat your heart out, Ruby. This one-liner filters a record collection for artists whose name matches "Bon Iver", and initializes an NSImage
from the album cover image data of the matching albums.
Is this a good idea? Probably not. (NSPredicate
is rad, and breaking complicated logic up is under-rated)
Is this insanely cool? You bet! This clever example has shown a possible direction for future Objective-C DSLs and meta-programming.
KVC Collection Operators are a must-know for anyone who wants to save a few extra lines of code and look cool in the process.
While scripting languages like Ruby boast considerably more flexibility in its one-liner capability, perhaps we should take a moment to celebrate the restraint built into Objective-C and Collection Operators. After all, Ruby is hella slow, amiright? </troll>