Blended Cocoa

Adventures in Objective-C, Swift and Cocoa

Objective-C Literals: Negative Array Subscripts

Several new features were added to the Apple LLVM Compiler 4.0 to allow the use of Objective-C Literals and object subscripts.

This means that it is now possible to use the following shorthands to create and access elements of an NSArray.

Objective-C Literal and Subscripting Syntax
1
2
3
NSArray *array = @[ @"World", @"Hello" ];

NSLog(@"%@ %@", array[1], array[0]);

During compilation the subscripted array access is converted to a call to the objectAtIndexedSubscript: method. So the above NSLog statement would be converted to the following before compilation.

Objective-C Subscripting Syntax
1
2
3
NSLog(@"%@ %@",
      [array objectAtIndexedSubscript:1],
      [array objectAtIndexedSubscript:0]);

This addition to the Objective-C language is useful and certainly will make some code less verbose and easier to read.

However, many languages (particularly scripting languages) allow the use of negative indexes. Negative indexes are counted from the end of the array rather than the beginning. This led me to wonder if you can use negative indexes with the new Objective-C syntax.

Unfortunately, a quick look at the method definition for objectAtIndexedSubscript: shows that the index parameter is an NSUInteger, an unsigned integer. Therefore, as implemented, the new subscript indexes must be positive.

objectAtIndexedSubscript: method definition
1
- (id)objectAtIndexedSubscript:(NSUInteger)idx;

So, is it possible to add functionality to NSArray to allow the use of negative indexes? The answer is Yes. The Objective-C runtime allows us to add the functionality to make it possible.

In order to do this we need to create our own version of the objectAtIndexedSubscript: method. The new method, BLC_objectAtIndexedSubscript: will look like:

The new objectAtIndexedSubscript: method
1
2
3
4
5
6
7
- (id)BLC_objectAtIndexedSubscript:(NSInteger)idx {
    if (idx < 0) {
        idx = [self count] + idx;
    }

    return [self BLC_objectAtIndexedSubscript:idx];
}

You will notice that the parameter is now an NSInteger rather than the unsigned NSUInteger used by the original method.

You may also have noticed what appears to be a recursive call in line 6. As things stand at the moment this is a recursive call but the next step is to perform a method swizzle.

Method swizzling involves swapping two implementations of a method, in our case objectAtIndexedSubscript: and BLC_objectAtIndexedSubscript:. We perform the method swizzle using the method_exchangeImplementations function of the Objective-C runtime, declared in the objc/runtime.h header file.

The method swizzle is performed as follows:

Method Swizzling objectAtIndexedSubscript:
1
2
3
4
method_exchangeImplementations(
    class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:)),
    class_getInstanceMethod(self, @selector(BLC_objectAtIndexedSubscript:))
);

Now that the method implementations have been swizzled that recursive call in BLC_objectAtIndexedSubscript: has become a call to the original method. The original method implementation is now called using the BLC_objectAtIndexedSubscript: selector.

Once we have swapped the methods we are able to use negative indexes as follows:

Using negative indexes
1
2
3
NSArray *array = @[ @"World", @"Hello" ];

NSLog(@"%@ %@", array[-1], array[-2]);

The full implementation of the new method and the method swizzling is contained in a category on NSArray. The full listing is shown below:

NSArray+NegativeIndexes.mView Gist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//
//  NSArray+NegativeIndexes.m
//  Allow Negative Array Literals
//
//  Created by Matthew Robinson on 25th October 2012.
//  Copyright (c) 2012 Matthew Robinson. All rights reserved.
//
//  WARNING: This is a proof of concept and should not be used
//           in production code.  This could break at anytime.

#import <Foundation/Foundation.h>

#if __has_feature(objc_subscripting)

#import <objc/runtime.h>

@implementation NSArray (NegativeIndexes)

+ (void)load {
    method_exchangeImplementations(
        class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:)),
        class_getInstanceMethod(self, @selector(BLC_objectAtIndexedSubscript:))
    );
}

- (id)BLC_objectAtIndexedSubscript:(NSInteger)idx {
    if (idx < 0) {
        idx = [self count] + idx;
    }

    return [self BLC_objectAtIndexedSubscript:idx];
}

@end

@implementation NSMutableArray (NegativeIndexes)

+ (void)load {
    method_exchangeImplementations(
        class_getInstanceMethod(self, @selector(setObject:atIndexedSubscript:)),
        class_getInstanceMethod(self, @selector(BLC_setObject:atIndexedSubscript:))
    );
}

- (void)BLC_setObject:(id)anObject atIndexedSubscript:(NSInteger)idx {
    if (idx < 0) {
        idx = [self count] + idx;
    }

    [self BLC_setObject:anObject atIndexedSubscript:idx];
}

@end

#endif

As you can see the above implementation also contains a category on NSMutableArray which swizzles a replacement method for setObject:atIndexedSubscript: to allow negative indexes in array assignment.

There is no associated header file for this category as the category does not add any new public methods to the NSArray (or NSMutableArray class). Simply compiling the above .m file into a binary will add the functionality to the array classes.

This category is available as a Gist at: https://gist.github.com/4076357

WARNING: This functionality is a hack and relies on the clang compiler converting array subscripts into the associated call to objectAtIndexedSubscript: without checking the positiveness of the index.

Comments