Как создать NSString из строки формата, такой как @ "xxx =% @, yyy =% @" и NSArray объектов? - PullRequest
29 голосов
/ 29 июня 2009

Есть ли способ создать новый NSString из строки формата, такой как @ "xxx =% @, yyy =% @" и NSArray объектов?

В классе NSSTring есть много методов, таких как:

- (id)initWithFormat:(NSString *)format arguments:(va_list)argList
- (id)initWithFormat:(NSString *)format locale:(id)locale arguments:(va_list)argList
+ (id)stringWithFormat:(NSString *)format, ...

но ни один из них не принимает NSArray в качестве аргумента, и я не могу найти способ создать va_list из NSArray ...

Ответы [ 13 ]

46 голосов
/ 30 июня 2009

На самом деле нетрудно создать va_list из NSArray. См. превосходную статью Мэтта Галлахера на эту тему.

Вот категория NSString, чтобы делать то, что вы хотите:

@interface NSString (NSArrayFormatExtension)

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments;

@end

@implementation NSString (NSArrayFormatExtension)

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments
{
    char *argList = (char *)malloc(sizeof(NSString *) * arguments.count);
    [arguments getObjects:(id *)argList];
    NSString* result = [[[NSString alloc] initWithFormat:format arguments:argList] autorelease];
    free(argList);
    return result;
}

@end

Тогда:

NSString* s = [NSString stringWithFormat:@"xxx=%@, yyy=%@" array:@[@"XXX", @"YYY"]];
NSLog( @"%@", s );

К сожалению, для 64-битных формат va_list изменился, поэтому приведенный выше код больше не работает. И, вероятно, не следует использовать в любом случае, поскольку это зависит от формата, который может быть изменен. Поскольку не существует действительно надежного способа создания va_list, лучшим решением будет просто ограничить число аргументов до разумного максимума (скажем, 10), а затем вызвать stringWithFormat с первыми 10 аргументами, что-то вроде этого:

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments
{
    if ( arguments.count > 10 ) {
        @throw [NSException exceptionWithName:NSRangeException reason:@"Maximum of 10 arguments allowed" userInfo:@{@"collection": arguments}];
    }
    NSArray* a = [arguments arrayByAddingObjectsFromArray:@[@"X",@"X",@"X",@"X",@"X",@"X",@"X",@"X",@"X",@"X"]];
    return [NSString stringWithFormat:format, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9] ];
}
35 голосов
/ 21 ноября 2012

На основании этого ответа с использованием автоматического подсчета ссылок (ARC): https://stackoverflow.com/a/8217755/881197

Добавьте категорию к NSString следующим способом:

+ (id)stringWithFormat:(NSString *)format array:(NSArray *)arguments
{
    NSRange range = NSMakeRange(0, [arguments count]);
    NSMutableData *data = [NSMutableData dataWithLength:sizeof(id) * [arguments count]];
    [arguments getObjects:(__unsafe_unretained id *)data.mutableBytes range:range];
    NSString *result = [[NSString alloc] initWithFormat:format arguments:data.mutableBytes];
    return result;
}
16 голосов
/ 30 июня 2009

Мне пришло в голову одно решение: я мог бы создать метод, работающий с фиксированным большим количеством аргументов, например:

+ (NSString *) stringWithFormat: (NSString *) format arguments: (NSArray *) arguments {
    return [NSString stringWithFormat: format ,
          (arguments.count>0) ? [arguments objectAtIndex: 0]: nil,
          (arguments.count>1) ? [arguments objectAtIndex: 1]: nil,
          (arguments.count>2) ? [arguments objectAtIndex: 2]: nil,
          ...
          (arguments.count>20) ? [arguments objectAtIndex: 20]: nil];
}

Я также мог бы добавить проверку, чтобы увидеть, имеет ли строка формата больше 21 символа '%', и в этом случае выдать исключение.

4 голосов
/ 29 января 2014

Этот ответ глючит. Как уже отмечалось, нет решения этой проблемы, которое гарантированно будет работать при внедрении новых платформ, кроме использования метода «10-элементный массив».


Ответ от solidsun работал хорошо, пока я не пошел на компиляцию с 64-битной архитектурой. Это вызвало ошибку:

EXC_BAD_ADDRESS тип EXC_I386_GPFLT

Решением было использование немного другого подхода для передачи списка аргументов методу:

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments;
{
     __unsafe_unretained id  * argList = (__unsafe_unretained id  *) calloc(1UL, sizeof(id) * arguments.count);
    for (NSInteger i = 0; i < arguments.count; i++) {
        argList[i] = arguments[i];
    }

    NSString* result = [[NSString alloc] initWithFormat:format, *argList] ;//  arguments:(void *) argList];
    free (argList);
    return result;
}

Это работает только для массивов с одним элементом

4 голосов
/ 29 июня 2009

@ Чак правильно о том факте, что вы не можете преобразовать NSArray в varargs . Тем не менее, я не рекомендую искать шаблон %@ в строке и заменять его каждый раз. (Замена символов в середине строки, как правило, довольно неэффективна, и не очень хорошая идея, если вы можете выполнить то же самое другим способом.) Вот более эффективный способ создания строки в описываемом вами формате:

NSArray *array = ...
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:[array count]];
for (id object in array) {
    [newArray addObject:[NSString stringWithFormat:@"x=%@", [object description]]];
}
NSString *composedString = [[newArray componentsJoinedByString:@", "] retain];
[pool drain];

Я включил пул автоматического выпуска для правильного ведения, так как для каждого элемента массива будет создана автоматически выпущенная строка, а изменяемый массив также будет автоматически освобожден. Вы можете легко превратить это в метод / функцию и вернуть composedString, не сохраняя его, и при желании обработать авторелиз в другом месте кода.

3 голосов
/ 30 августа 2015

Для тех, кому нужно решение Swift, вот расширение для этого в Swift

extension String {

    static func stringWithFormat(format: String, argumentsArray: Array<AnyObject>) -> String {
        let arguments = argumentsArray.map { $0 as! CVarArgType }
        let result = String(format:format, arguments:arguments)
        return result
    }

}
3 голосов
/ 29 июня 2009

Там нет общего способа передать массив в функцию или метод, который использует varargs. В этом конкретном случае, однако, вы можете подделать его, используя что-то вроде:

for (NSString *currentReplacement in array)
    [string stringByReplacingCharactersInRange:[string rangeOfString:@"%@"] 
            withString:currentReplacement];

РЕДАКТИРОВАТЬ: Принятый ответ утверждает, что есть способ сделать это, но независимо от того, насколько хрупким может показаться этот ответ, этот подход гораздо более хрупким. Он основан на поведении, определяемом реализацией (в частности, структуре va_list), которое не гарантируется тем же. Я утверждаю, что мой ответ правильный, и предложенное мной решение менее хрупкое, поскольку оно опирается только на определенные особенности языка и структур.

2 голосов
/ 30 июня 2009

Да, это возможно. В GCC, ориентированном на Mac OS X, по крайней мере, va_list - это просто массив C, так что вы сделаете один из id s, а затем попросите NSArray заполнить его:

NSArray *argsArray = [[NSProcessInfo processInfo] arguments];
va_list args = malloc(sizeof(id) * [argsArray count]);
NSAssert1(args != nil, @"Couldn't allocate array for %u arguments", [argsArray count]);

[argsArray getObjects:(id *)args];

//Example: NSLogv is the version of NSLog that takes a va_list instead of separate arguments.
NSString *formatSpecifier = @"\n%@";
NSString *format = [@"Arguments:" stringByAppendingString:[formatSpecifier stringByPaddingToLength:[argsArray count] * 3U withString:formatSpecifier startingAtIndex:0U]];
NSLogv(format, args);

free(args);

Вы не должны полагаться на эту природу в коде, который должен быть переносимым. Разработчики iPhone, это одна вещь, которую вы обязательно должны протестировать на устройстве.

1 голос
/ 16 ноября 2016
- (NSString *)stringWithFormat:(NSString *)format andArguments:(NSArray *)arguments {
    NSMutableString *result = [NSMutableString new];
    NSArray *components = format ? [format componentsSeparatedByString:@"%@"] : @[@""];
    NSUInteger argumentsCount = [arguments count];
    NSUInteger componentsCount = [components count] - 1;
    NSUInteger iterationCount = argumentsCount < componentsCount ? argumentsCount : componentsCount;
    for (NSUInteger i = 0; i < iterationCount; i++) {
        [result appendFormat:@"%@%@", components[i], arguments[i]];
    }
    [result appendString:[components lastObject]];
    return iterationCount == 0 ? [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] : result;
}

Протестировано с форматом и аргументами:

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[@"XXX", @"YYY", @"ZZZ"];

Результат: xxx = XXX, yyy = YYY последний компонент

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[@"XXX", @"YYY"];

Результат: xxx = XXX, yyy = YYY последний компонент

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[@"XXX"];

Результат: xxx = XXX последний компонент

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[];

Результат: последний компонент

NSString *format = @"some text";
NSArray *arguments = @[@"XXX", @"YYY", @"ZZZ"];

Результат: некоторый текст

0 голосов
/ 20 июня 2015

Можно создать категорию для NSString и создать функцию, которая получает формат, массив и возвращает строку с замененными объектами.

@interface NSString (NSArrayFormat)

+ (NSString *)stringWithFormat:(NSString *)format arrayArguments:(NSArray *)arrayArguments;

@end

@implementation NSString (NSArrayFormat)

+ (NSString *)stringWithFormat:(NSString *)format arrayArguments:(NSArray *)arrayArguments {
    static NSString *objectSpecifier = @"%@"; // static is redundant because compiler will optimize this string to have same address
    NSMutableString *string = [[NSMutableString alloc] init]; // here we'll create the string
    NSRange searchRange = NSMakeRange(0, [format length]);
    NSRange rangeOfPlaceholder = NSMakeRange(NSNotFound, 0); // variables are declared here because they're needed for NSAsserts
    NSUInteger index;
    for (index = 0; index < [arrayArguments count]; ++index) {
        rangeOfPlaceholder = [format rangeOfString:objectSpecifier options:0 range:searchRange]; // find next object specifier
        if (rangeOfPlaceholder.location != NSNotFound) { // if we found one
            NSRange substringRange = NSMakeRange(searchRange.location, rangeOfPlaceholder.location - searchRange.location);
            NSString *formatSubstring = [format substringWithRange:substringRange];
            [string appendString:formatSubstring]; // copy the format from previous specifier up to this one
            NSObject *object = [arrayArguments objectAtIndex:index];
            NSString *objectDescription = [object description]; // convert object into string
            [string appendString:objectDescription];
            searchRange.location = rangeOfPlaceholder.location + [objectSpecifier length]; // update the search range in order to minimize search
            searchRange.length = [format length] - searchRange.location;
        } else {
            break;
        }
    }
    if (rangeOfPlaceholder.location != NSNotFound) { // we need to check if format still specifiers
        rangeOfPlaceholder = [format rangeOfString:@"%@" options:0 range:searchRange];
    }
    NSAssert(rangeOfPlaceholder.location == NSNotFound, @"arrayArguments doesn't have enough objects to fill specified format");
    NSAssert(index == [arrayArguments count], @"Objects starting with index %lu from arrayArguments have been ignored because there aren't enough object specifiers!", index);
    return string;
}

@end

Поскольку NSArray создается во время выполнения, мы не можем предоставлять предупреждения во время компиляции, но мы можем использовать NSAssert, чтобы сообщить нам, равно ли число спецификаторов количеству объектов в массиве.

Создан проект на Github, где можно найти эту категорию. Также добавлена ​​версия Чака с использованием 'stringByReplacingCharactersInRange:' плюс несколько тестов.

Используя миллион объектов в массиве, версия с 'stringByReplacingCharactersInRange:' не очень хорошо масштабируется (подождала около 2 минут, затем закрыла приложение). Используя версию с NSMutableString, функция создала строку примерно за 4 секунды. Тесты проводились с использованием симулятора. Перед использованием тесты должны проводиться на реальном устройстве (используйте устройство с наименьшими характеристиками).

Редактировать: На iPhone 5s версия с NSMutableString занимает 10,471655 с (один миллион объектов); на iPhone 5 занимает 21.304876s.

...