mamcx
mamcx

Reputation: 16186

Is possible send a array in Obj-c for a variable arguments function?

In python it is easy to build a dictionary or array and pass it unpacked to a function with variable parameters

I have this:

- (BOOL) executeUpdate:(NSString*)sql, ... {

And the manual way is this:

[db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" ,
  @"hi'", // look!  I put in a ', and I'm not escaping it!
  [NSString stringWithFormat:@"number %d", i],
  [NSNumber numberWithInt:i],
  [NSDate date],
  [NSNumber numberWithFloat:2.2f]];

But I can't hardcode the parameters I'm calling, I want:

NSMutableArray *values = [NSMutableArray array];

for (NSString *fieldName in props) {
  ..
  ..
  [values addObject : value]
}
[db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" ,??values];

Upvotes: 12

Views: 12073

Answers (8)

johnboiles
johnboiles

Reputation: 3524

Chuck is right, there's no proper argument unpacking in Objective-C. However, for methods that require nil termination (NS_REQUIRES_NIL_TERMINATION), you can expand the variable list larger than what is needed by using an array accessor that returns nil when index >= count. This is most certainly a hack, but it works.

// Return nil when __INDEX__ is beyond the bounds of the array
#define NSArrayObjectMaybeNil(__ARRAY__, __INDEX__) ((__INDEX__ >= [__ARRAY__ count]) ? nil : [__ARRAY__ objectAtIndex:__INDEX__])

// Manually expand an array into an argument list
#define NSArrayToVariableArgumentsList(__ARRAYNAME__)\
NSArrayObjectMaybeNil(__ARRAYNAME__, 0),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 1),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 2),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 3),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 4),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 5),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 6),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 7),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 8),\
NSArrayObjectMaybeNil(__ARRAYNAME__, 9),\
nil

Now you can use NSArrayToVariableArgumentsList wherever you expect a nil-terminated variable argument list (as long as your array is smaller than 10 elements). For example:

NSArray *otherButtonTitles = @[@"button1", @"button2", @"button3"];
UIActionSheet *actionSheet = [[self alloc] initWithTitle:@"Title"
                                                delegate:self
                                       cancelButtonTitle:@"Cancel"
                                  destructiveButtonTitle:nil
                                       otherButtonTitles:NSArrayToVariableArgumentsList(otherButtonTitles)];

Upvotes: 18

unsynchronized
unsynchronized

Reputation: 4938

additionally to robottobor's solution: if you add the following macro:

#define splitAlternatingArray(args,arg1,arg2) \
NSMutableArray *arg1 = [NSMutableArray array];\
NSMutableArray *arg2 = [NSMutableArray array];\
{\
  BOOL isFirst = YES;\
  for (id arg in args) {\
    if (isFirst) {\
        [arg1 addObject:arg];\
    } else {\
        [arg2 addObject:arg];\
    }\
    isFirst = !isFirst;\
  }\
}

you can then do tricky things like:

- (id)initWithObjectsAndKeys:(id)firstObject, ...{
    NSArray *objKeyArray = VarArgs2(firstObject);
    splitAlternatingArray(objKeyArray,objs,keys);
    return [self initWithObjects:objs forKeys:keys];
}

Upvotes: 0

Dmitry
Dmitry

Reputation: 1041

You should use new FMDB version http://github.com/ccgus/fmdb. It has the method you need:

- (BOOL) executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;

Upvotes: 2

Vais Salikhov
Vais Salikhov

Reputation:

There is a nice example how you can go from NSArray to va_list here (see "va_list in Cocoa" and "Creating a fake va_list" sections towards the bottom):

http://cocoawithlove.com/2009/05/variable-argument-lists-in-cocoa.html

Here is a teaser ("arguments" is NSArray):

char *argList = (char *)malloc(sizeof(NSString *) * [arguments count]);
[arguments getObjects:(id *)argList];
contents = [[NSString alloc] initWithFormat:formatString arguments:argList];
free(argList);

Not quite python or ruby, but hey...

Upvotes: 3

robottobor
robottobor

Reputation: 11762

I wanted to do the same thing. I came up with the following, which works fine, given some constraints on the input variables.

NSArray* VarArgs(va_list ap)
{
  id obj;
  NSMutableArray* array = [NSMutableArray array];

  while ((obj = va_arg(ap, id))) {
    [array addObject:obj];
  }
  return array;
}

#define VarArgs2(_last_) ({ \
  va_list ap; \
  va_start(ap, _last_); \
  NSArray* __args = VarArgs(ap); \
  va_end(ap); \
  if (([__args count] == 1) && ([[__args objectAtIndex:0] isKindOfClass:[NSArray class]])) { \
    __args = [__args objectAtIndex:0]; \
  } \
__args; })

Using the above, I can call the following with either an NSArray or with varargs.

// '...' must be objc objects with nil sentinel OR an NSArray with nil sentinel
- (void)someMethod:(NSString *)sql, ...
{
   NSArray *args = VarArgs2(sql);

   // Do stuff with args
}

One more tip is to use the following in the prototype to have the compiler check for the nil sentinel to avoid potential bad things. I got this out of the apple headers...

- (void)someMethod:(NSString *)sql, ... NS_REQUIRES_NIL_TERMINATION;

Upvotes: 4

Timo Metsälä
Timo Metsälä

Reputation: 366

Unfortunately (Objective-)C doesn't provide a way to do that. The executeUpdate method would need to accept an NSArray instead of variable argument list in this case.

However, if you do know the amount of entries in the array (you have the amount in the string in the example anyway), you can of course do something like

[db executeUpdate:@"insert into test (a, b) values (?, ?)", [values objectAtIndex:0], [values objectAtIndex:1]]

If executeUpdate is an external library method and that library does not offer a version of the method accepting an NSArray, you could come up with your own wrapper function. The function would take the query string and an array as argument. This function would then call the executeUpdate method with correct amount of arguments based on the length of the array, something along the lines of

if ([values count] == 1) {
  [db executeUpdate:query, [values objectAtIndex:0]];
}
else if ([values count] == 2) {
  [db executeUpdate:query, [values objectAtIndex:0], [values objectAtIndex:1]];
}

you could then call this new function as

executeUpdateWrapper(@"insert into test (a, b) values (?, ?)", values);

The obvious drawback in this solution is that you need to handle all possible lengths of the array separately in the function and it has a lot of copy-paste code.

Upvotes: 1

Chuck
Chuck

Reputation: 237080

Unfortunately, no. Objective-C doesn't have argument unpacking like you get in a lot of modern languages. There isn't even a good way to work around it that I've ever found.

Part of the problem is that Objective-C is essentially just C. It does multiple argument passing with C varargs, and there's no simple way to do this with varargs. A relevant SO discussion.

Upvotes: 15

mipadi
mipadi

Reputation: 410932

To accomplish what you want, you have to use "varargs", as your method uses, or you can pass in an array of values, something like [db executeUpdate:sql withValues:vals];, and then pull out the values in the method. But there's no way to do something more "Pythonic", such as automatically unpacking a tuple of values, á la def executeUpdate(sql, *args).

Upvotes: 1

Related Questions