在传递之前处理varargs

时间:2012-03-07 10:17:14

标签: objective-c variadic-functions

我想构建一个与NSString的{​​{1}}具有相同签名的函数(使用varargs),但是我希望在传递它之前对每个参数进行URL编码到stringWithFormat:本身。我已经有一个方法stringWithFormat:来执行编码。如何使用编码参数构建va_list?

2 个答案:

答案 0 :(得分:1)

在C,C ++或Objective-C中没有可移植的方法来构建va_lists。您可能会编写或查找一些使用内联汇编的函数来直接修改堆栈并进行可变参数调用,但这确实不是一个好方法。你有三个我能想到的实用选择。

这是第一个选择。仅使用可变字符串并使用NSString的initWithFormat:arguments:方法转发参数。

- (NSString*)forwardMessage:(NSString*)format, ... {
    va_list args;
    va_start(args, format);

    BOOL escape = NO;
    char* ptr = (char*)[format UTF8String];
    while (*ptr) {
        if (*ptr == '%') {
            escape = !escape;
        } else if (escape) {
            // argument
            id obj = va_arg(args, id);
            if (*ptr == '@') {
                // object
                if ([obj isKindOfClass:[NSString class]]) {
                    // string
                    id copy = [obj copy];
                    if (copy != obj) {
                        // mutable
                        [obj replaceCharactersInRange:NSMakeRange(0, [obj length]) withString:@"replaced!"];
                    }
                }
            }
            escape = NO;
        }
        ++ptr;
    }

    va_end(args);

    va_list args2;
    va_start(args2, format);

    NSString* ret = [[NSString alloc] initWithFormat:format arguments:args2];

    va_end(args2);
    return ret;
}

该方法将采用可变参数,并将使用“replacement!”替换任何可变字符串的内容。因为我们只能在转发它们之前读取参数,所以我们实际上不能将不同的对象发送到initWithFormat:arguments:我们只需要改变对象。请注意,制作一个对象的副本以测试它是否像我在该方法中那样可变是不太好的做法。

这是你的第二个选择。使用NSInvocation为stringWithFormat构建新参数:。

- (NSString*)forwardMessage:(NSString*)format, ... {
    BOOL escape = NO;
    NSUInteger count = 0;

    char* ptr = (char*)[format UTF8String];
    while (*ptr) {
        if (*ptr == '%') {
            escape = !escape;
        } else if (escape) {
            if (*ptr == '@') {
                // this is an object
            }
            ++count;
            escape = NO;
        }
        ++ptr;
    }

    char* sig = malloc(3 + count + 2);
    memset(sig, '@', 3 + count);
    sig[3 + count] = ':';
    sig[3 + count + 1] = '\0';

    NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:sig]];

    free(sig);

    [invocation setTarget:[NSString class]];
    [invocation setSelector:@selector(stringWithFormat:)];

    [invocation setArgument:&format atIndex:2];

    va_list args;
    va_start(args, format);

    for (NSUInteger i = 0; i < count; ++i) {
        void* arg = va_arg(args, void*);
        // arg is an object, you can change it here
        [invocation setArgument:&arg atIndex:i + 3];
    }

    [invocation invoke];

    va_end(args);

    id ret;

    [invocation getReturnValue:&ret];

    return ret;
}

该方法确实有一个缺点:当您传递大小与void *或id不同的类型时,它将无法工作。例如,整数有效,但浮点数不正确。 [self forwardMessage:@"test %d asd %s %f %@ %d", 2, "asd", 2.897, @"test" 5]返回test 2 asd asd 0.000000 test 5。添加更多逻辑以使特定类型正常工作并不会太难。

我不确定我是否真的可以推荐其中任何一种解决方案,但也许它们可以适合您的情况。

第三个选项:如果你的参数只是NSString对象,我将不得不建议放弃stringWithFormat:并自己解析/形成格式化的字符串。

答案 1 :(得分:0)

我认为这很难做得很好。问题是,你无法创建va_list; “函数”va_startva_end等被定义为宏,隐藏了丑陋的内部东西,你可能不想弄脏你的东西。

如果你真的想这样做,你可能会解析format字符串的内容(不应该非常困难,特别是如果你只假设%@是可能的格式化说明符),所以你知道va_list中有多少个参数,然后是encode每个参数并返回最终的重建字符串。当然,你正在重新实现stringWithFormat:然后,然而。

“互联网”告诉我构建va_list没有可移植的方式,但如果可以的话,您将能够使用以下想法:

s = [[[NSString alloc] initWithFormat:format arguments:argp] autorelease];

我个人认为,最好的办法就是在遇到va_arg令牌时使用%@遍历字符串并构建结果。

[编辑]这是关于@daxnitro在下面所说的内容,我建议的方法,他称之为选项3:这是一个快速的黑客,但只有对象标记,它才有效。

typedef enum {
NORMAL,
TOKEN
} STATE;

+ (NSString *) encodeWithFormat: (id) format, ... {

STATE s = NORMAL;
va_list argp;
va_start(argp, format);
unichar c;
NSString * tmp;
NSMutableString * out = [[NSMutableString alloc] init];

for(int i = 0; i < [format length]; i ++) {

    c = [format characterAtIndex: i];

    // simple state-based recognising
    switch(c) {
    case '%':
        if(s == NORMAL)
            s = TOKEN; // switch to token-mode
        else { // we were accepting tokens, so this is an escaped '%'
            [out appendFormat: @"%c", c];
            s = NORMAL;
        }
        break;
    default:
        if(s == NORMAL) // default case
            [out appendFormat: @"%c", c];
        else // accepting tokens, so check type
            switch(c) {
            case '@': // this is a string placeholder
                tmp = va_arg(argp, NSString*);
                [out appendFormat: @"%@", [Test encode:tmp]]; // your magic here
                s = NORMAL;
                break;
            // you could add cases for %d etc here, if necessary
            default: // some unrecognised placeholder. ignore.
                s = NORMAL;
            break;
            }
        break;
    }
}
va_end(argp);
return [out autorelease];
}