Objective-C Literals, Part 1

Mark Dalrymple's Headshot
Mark Dalrymple

Update : Official Documentation now

Looks like the cat is out of the bag, the new Objective-C literals have landed into the clang trunk. This change introduces additional Objective-C Literals.

What is an "Objective-C Literal"? It's a chunk of code that references a specific Cocoa object, creating it if necessary. Objective-C has had literals for NSStrings since the beginning of time:

    NSString *greeble = @"Bork Bork Bork";

The syntax is an at-sign, the indication that Objective-C magic is approaching, in front of a C-string literal. This makes an NSString that can be used just like any other NSString. In fact, greeble points to an actual NSString, it's not any kind of second-class citizen. The new literals for NSNumbers, NSArrays, and NSDictionaries take a similar approach

NSNumber

You can't put scalar types like ints or floats directly into Foundation collections. You have to "box" them first by wrapping them in an NSNumber object. @-prefixing a numerical literal value will automatically create an NSNumber object that wraps (or boxes) that number:

    NSArray *array =
        [NSArray arrayWithObjects: @0, @23, @3.1415, @25.101, nil];
    NSLog (@"array: %@", array);

Will print out

array: (
    0,
    23,
    "3.1415",
    "25.101"
)

You can see that integers and floats are wrapped. You can also wrap the Objective-C symbols YES and NO:

    NSLog (@"YES: %@  NO: %@", @YES, @NO);

And it prints out what you'd expect:

YES: 1  NO: 0

You cannot wrap NULL, which gives an "error: unexpected '@' in program" error instead. It would be nice if @NULL would be equivalent to [NSNull null], so dupe Radar 10887680 if you agree.

Even with this new syntax, there is no "autoboxing" in Cocoa. Autoboxing in languages like Java means that primitive scalar types get automatically converted to NSNumber equivalents and back. The Objective-C number literals are just syntactic sugar for making NSNumbers. If you get a numeric value from somewhere other than directly from your source code you'll need to box it up yourself before you do something with it, like putting it into a Cocoa collection, sending it through Key-Value Coding, or passing it to a method or function that's expecting an NSNumber.

NSArray

Most scripting languages have ways of creating arrays directly in source code or via the language's REPL (run-evaluate-print-loop), usually by specifying the array contents in square brackets. Objective-C can now do that with similar syntax, square-brackets preceded by an at-sign:

    NSArray *gronk = @[ @"hi", @"bork", @23, @YES ];

And printing it yields contents of what you expect:

(
    hi,
    bork,
    23,
    1
)

Which is more concise than the usual NSArray creation methods.

Notice you don't need a trailing nil here. The compiler knows how many objects you're using to create the array so there's no need for a sentinel value at the end. nil is not an object, so the compiler will happily give you an error if you try to put one there anyway.

You're not limited to literal objects inside of the array:

    NSString *thing1 = [NSString stringWithFormat: @"thing1"];
    NSString *thing2 = [NSMutableString stringWithString: @"thing2"];
    NSOperation *op = [NSBlockOperation blockOperationWithBlock: ^{ }];

    NSArray *stuff = @[ thing1, thing2, op ];

    NSLog (@"%@", stuff);

Prints out

(
    thing1,
    thing2,
    "<NSBlockOperation: 0x7fb60ae009f0>"
)

Under the hood it's just calling -arrayWithObjects:count: to create the array, so you will get an exception if you try to sneak in a nil value through an object pointer.

NSArray Access Syntax

There is new syntax for accessing the contents of an array. Take an NSArray or NSMutableArray pointer and subscript it with square brackets:

    // Make a new array
    NSArray *gronk = @[ @"hi", @"bork", @23, @YES ];

    NSLog (@"gronk 0: %@", gronk[0]);
    NSLog (@"gronk 2: %@", gronk[2]);
    NSLog (@"gronk 37: %@", gronk[37]);

This prints out two array elements:

gronk 0: hi
gronk 2: 23

And generates a range exception for the third one:

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** 
    -[__NSArrayI objectAtIndex:]: index 37 beyond bounds [0 .. 3]'

You can chain the indexing as well if you have arrays inside of arrays:

    NSArray *nesting = @[ @"hi", @[ @"ohai" ] ];
    NSLog (@"nesting: %@", nesting[1][0]);

This gets the second element of the nesting array, which is an array with a single element. And then it gets the first element of that interior array:

nesting: ohai

This new indexing syntax also works with NSOrderedSet.

The indexing works for mutable arrays on the other side of the assignment operator. You can use it to change existing arrays:

    NSMutableArray *mutt = 
        [NSMutableArray arrayWithArray: @[ @"A", @"B", @"C"] ];        
    NSLog (@"before %@", mutt);

Our original array is:

before (
    A,
    B,
    C
)


And then make some changes:

    mutt[0] = @"aaa";                                                                       
    mutt[3] = @"d";                                                                       
    mutt[4] = @"e";

    [mutt insertObject: @"f"  atIndex: 5];

    NSLog (@"after %@", mutt);

Which prints out:

after (
    aaa,
    B,
    C,
    d,
    e,
    f
)

You can see that some elements (d-f) were inserted at the end of the array. You can insert new objects this way by setting the value one past the very last element, just like you saw with -insertObject:atIndex:

NSDictionary

NSDictionary is the final object to get new literal syntax. Instead of wrapping a set of objects with square brackets, you wrap them with braces preceded by an at-sign. The key is listed first, then a colon, then the value.

    NSDictionary *splunge = @{ @"hi" :  @"bork", @"greeble" :  @"bork" };
    NSLog (@"splunge %@", splunge);

This creates a new dictionary with the keys hi and greeble, each with the value bork:

splunge {
    greeble = bork;
    hi = bork;
} 

The key/value order is reverse from what NSDictionary's -dictionaryWithObjectsAndKeys: uses, so be careful if you decide to mechanically update existing code to use the new syntax.

You can of course nest dictionaries arbitrarily deep, and include literal arrays as values.

It's easy to imagine that this can lead to concise creation of dictionaries. Suppose you had an NSView prints some text in its -drawRect: with various text attributes. Here is the old-style of defining the attributes:

    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
        [NSFont userFontOfSize: 150.0],  NSFontAttributeName,
        [NSNumber numberWithFloat: 3.0], NSStrokeWidthAttributeName,
        [NSColor blueColor],             NSStrokeColorAttributeName,
        [NSNumber numberWithInt: 10],    NSKernAttributeName,
        [NSNumber numberWithFloat: 0.5], NSObliquenessAttributeName,
        nil];

and the new-style, in with the rest of the drawing code:

- (void) drawRect: (CGRect) rect {

    NSString *text = @"Ponies!";

    NSDictionary *attributes = @{
        NSFontAttributeName        : [NSFont userFontOfSize: 150.0],
        NSStrokeWidthAttributeName : @3.0,
        NSStrokeColorAttributeName : [NSColor blueColor],
        NSKernAttributeName        : @10,
        NSObliquenessAttributeName : @0.5
    };    

    [text drawAtPoint: CGPointMake(10.0, 100.0)  withAttributes: attributes];

} // drawRect

And the resulting view drawing:

Ponies

NSDictionary Access Syntax

NSDictionary also grows the same accessor syntax that arrays have: square brackets after a dictionary pointer lets you access stuff. Instead of passing in a scalar index, you pass in an object to be used as the lookup key.

Given our splunge dictionary from earlier:

    NSDictionary *splunge = @{ @"hi" :  @"bork", @"greeble" :  @"bork" };

You can dig into it with the square brackets:

    NSLog (@"%@", splunge[@"greeble"]);
    NSLog (@"%@", splunge[@"ACK"]);

which prints out:

bork
(null)

The value for the key greeble is "bork", and there is no value for the key ACK, so nil (printed out (null)) is returned.

You can of course nest dictionaries and access them:

    NSDictionary *nest = @{ @"hi" : @"ohai",
                            @"ichc" :  @{ @"oop" : @"ack"} };
    NSLog (@"nest: %@", nest);
    NSLog (@"noost: %@", nest[@"ichc"][@"oop"]);
    NSLog (@"nist: %@", nest[@"ichc.oop"]);  // try a key path

which prints out:

nest: {
    hi = ohai;
    ichc = {
        oop = ack;
    };
}
noost: ack
nist: (null)

You can see the nested dictionaries in the first lines of output. Then there are stacked square brackets that first grab the ichc dictionary, and then the next square brackets get the oop key out of that.

The key used for dictionary lookup is not a key path. Key-Value Coding is not happening under the hood, which you can by see it returning nil when trying to treat the two keys used in the square brackets as a key path.

Like with arrays, you can change mutable dictionaries using the array syntax.

    NSDictionary *flarn = @{ @"name" :  @"Hoover",  @"age" :  @42 };
    NSMutableDictionary *mutaflarn = [flarn mutableCopy];

    mutaflarn[@"name"] = @"Dr. Manhattan";
    NSLog (@"mutaflarn: %@", mutaflarn);

You can see from the output that the name has changed:

mutaflarn: {
    age = 42;
    name = "Dr. Manhattan";
}

Limitations

There are some limitations with Objective-C literals. I don't know if these are permanent limitations, or just an in-progress implementation.

Just like with NSStrings, collection objects made via literal arrays and dictionaries are immutable. You cannot create mutable collections with the literal syntax, so you will have to make a mutable copy after making the immutable dictionary or array.

You also cannot have static initializers like you can with@"NSStrings". For example, check out the start of this source file:

#import <Foundation/Foundation.h>

NSString *thing = @"thing 1";
NSArray *array = @[ @"thing 2" ];

@implementation BNRGroovyObject
...

The static NSString initializer works as always, but the array initializer gives an error because the array is being created outside of a function or a method body:

literal.m:4:18: error: initializer element is not a compile-time constant
NSArray *array = @[ @"thing 2" ];
                 ^~~~~~~~~~~~~~~

Of the collection classes, only arrays and dictionaries can be created literally. Literal NSSets aren't supported.

Finally, you can't use negative indexes for arrays. A typical scripting language trick of using array[-1] to access the last element throws an out-of-range exception with a huge index, which is understandable because a negative signed-value being treated as an unsigned-value by NSArray's -objectAtIndex:.

Be sure to come back Monday when we'll resume our tour of Objective-C literalness.

Recent Comments

comments powered by Disqus