Search code examples
objective-cmethodsvariadicvariadic-functions

Using a va_list method without a count


I'm writing a category on NSArray to add JavaScript array methods to NSArray. In JavaScript, the splice() method both adds/removes items to/from an array. But the number of objects added to the array may vary. So I used va_list to allow for a more flexible input of object values.

As it stands the method requires a count input value. How could I rewrite this without one?

Interface

@interface NSArray (JavaScriptArray)

- (NSArray *)splice:(NSUInteger)index remove:(NSUInteger)remove count:(NSUInteger)count arguments:(id)firstObject,...;

@end

Implementation

@implementation NSArray (JavaScriptArray)

- (NSArray *)splice:(NSUInteger)index remove:(NSUInteger)remove count:(NSUInteger)count arguments:(id)firstObject,...
{
    NSMutableArray *mSplice = [NSMutableArray arrayWithArray:self];

    if (remove != 0) {
        NSUInteger removeIndex = index;

        for (NSUInteger i = 0; i < remove; i++) {
            [mSplice removeObjectAtIndex:removeIndex];
            removeIndex = removeIndex + 1;
        }
    }

    if (count != 0) {
        NSUInteger addIndex = index;

        id eachObject;
        va_list argumentList;
        if (firstObject) {
            [mSplice insertObject:firstObject atIndex:addIndex];
            addIndex = addIndex + 1;
            va_start(argumentList, firstObject);
            eachObject = va_arg(argumentList, id);

            for (NSUInteger i = 0; i < count; i++) {
                [mSplice insertObject:eachObject atIndex:addIndex];
                 addIndex = addIndex + 1;
            }

            va_end(argumentList);
        }

    }

    return [NSArray arrayWithArray:mSplice];
}

@end

Calling the Method

- (void)viewDidLoad 
{
    [super viewDidLoad];

    NSArray *fruit = @[@"Banana", @"Orange", @"Apple", @"Mango"];

    NSArray *fruitSplice = [fruit splice:2 remove:0 count:4 arguments:@"Lemon", @"Kiwi", @"Kiwi", @"Kiwi"];
    NSLog(@"fruitSplice %@", fruitSplice);
}

@end

Debugger Window

fruitSplice (
             Banana,
             Orange,
             Lemon,
             Kiwi,
             Kiwi,
             Kiwi,
             Kiwi,
             Apple,
             Mango
             )

Solution

  • Removing the count argument is no problem, because actually your "add" loop appears to be incorrect. After you've gotten the second item from the va_list with eachObject = va_arg(argumentList, id);, you never get another object. The only reason your example works is that all the later items are the same: @"Kiwi". If your test call was

    NSArray *fruitSplice = [fruit splice:2 
                                  remove:0 
                                   count:4 
                               arguments:@"Lemon", @"Albatross", @"Kiwi", @"Kiwi"];
    

    you'd see

    fruitSplice (
                 Banana,
                 Orange,
                 Lemon,
                 Albatross,
                 Albatross,
                 Albatross,
                 Albatross,
                 Apple,
                 Mango
                 )
    

    as output.

    You need to redo your unpacking of the va_list, and you can keep your own counter while you're iterating it, but the catch is that there has to be a sentinel value: an value that can't possibly appear as a valid list value, that indicates you've come to the end. For object-type variadic arguments you would generally use nil as the sentinel.

    When you're using a va_list you must have either a sentinel or a count. There's no other way for you to know when to stop popping arguments.

    Your signature can become*:

    - (NSArray *)KRSpliceAt:(NSUInteger)index 
              removingCount:(NSUInteger)remove 
              addingObjects:(id)firstObject, ... NS_REQUIRES_NIL_TERMINATION;
    

    The NS_REQUIRES_NIL_TERMINATION is strictly optional, but will make the compiler notify you if the method is called without a sentinel.

    Then your adding loop changes:

    // Insertion index starts at given splice point
    NSUInteger addIndex = index;
    // Initialize the va_list
    va_list objs;
    va_start(objs, firstObj);
    // Start at the beginning
    id nextObj = firstObj;
    // Test for sentinel nil
    while( nextObj ){
        [mSplice insertObject:nextObj atIndex:addIndex];
        // Update insertion point
        addIndex++;
        // Get next argument
        nextObj = va_arg(objs, id);
    }
    // Signal completion of list
    va_end(objs);
    

    *Methods you add to classes you don't own should always be prefixed. It's a bit annoying, but it's good practice to prevent a catastrophic clash if you should happen to pick the same name as another method.