Обработка varargs перед передачей их - PullRequest
4 голосов
/ 07 марта 2012

Я хотел бы создать функцию с такой же сигнатурой, как у NSString stringWithFormat: (с переменными), но я бы хотел, чтобы URL закодировал каждый аргумент, прежде чем передать его в stringWithFormat: сам. У меня уже есть метод + (NSString *)urlEncode:(NSString *)s, который выполняет кодирование. Как мне построить va_list с закодированными аргументами?

Ответы [ 2 ]

2 голосов
/ 07 марта 2012

Нет переносимого способа создания va_lists в C, C ++ или Objective-C. Возможно, вы могли бы написать или найти некоторые функции, которые используют встроенную сборку для прямой модификации стека и выполнения различных вызовов, но на самом деле это не очень хороший способ. У меня есть три практических варианта, о которых я могу подумать.

Вот первый вариант. Используйте только изменяемые строки и передавайте аргументы, используя initSithFormat: arguments: метод NSString.

- (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;
}

Этот метод будет принимать переменные аргументы и заменяет содержимое любой изменяемой строки словом «replace!». Поскольку мы можем только прочитать аргументы перед их пересылкой, мы не можем на самом деле отправлять разные объекты в 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: и проанализировать / сформировать форматированную строку самостоятельно.

0 голосов
/ 07 марта 2012

Я думаю, что это довольно сложно сделать красиво. Дело в том, что вы не можете создать va_list; «функции» va_start, va_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];
}
...