Как создать NSString из строки формата типа @“xxx=%@, yyy=%@” и NSArray объектов?

StackOverflow https://stackoverflow.com/questions/1058736

Вопрос

Есть ли какой-нибудь способ создать новую 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...

Это было полезно?

Решение

На самом деле создать 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] ];
}

Другие советы

Основываясь на этом ответе с использованием автоматического подсчета ссылок (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;
}

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

+ (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 символа '%', и в этом случае выдать исключение.

@Чак верно ли то, что вы не удается преобразовать 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 не сохраняя его, и при желании обработайте авторелиз в другом месте кода.

Этот ответ глючит.Как уже отмечалось, не существует решения этой проблемы, которое гарантированно работало бы при внедрении новых платформ, кроме использования метода "массива из 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;
}

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

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

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

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

Для тех, кому нужно быстрое решение, вот расширение, позволяющее сделать это в 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
    }

}

Да, это возможно.В GCC, ориентированном на Mac OS X, по крайней мере, va_list это просто массив C, так что вы создадите один из ids, затем скажите 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, это одна вещь, которую вы обязательно должны протестировать на устройстве.

- (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=ХХХ, гггг=ГГГГ последний компонент

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

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

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"];

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

Я нашел некоторый код в Интернете, который утверждает, что это возможно, однако мне не удалось сделать это самому, однако, если вы не знаете количество аргументов заранее, вам также нужно динамически создавать строку формата, поэтому я просто не вижу смысла.

Вам лучше просто создать строку, повторив массив.

Вы могли бы найти stringByAppendingString:или stringByAppendingFormat:удобный метод экземпляра .

Можно создать категорию для 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,304876 секунды.

Вот ответ без явного создания массива:

   NSString *formattedString = [NSString stringWithFormat:@"%@ World, Nice %@", @"Hello", @"Day"];

Первая строка - это целевая строка, подлежащая форматированию, следующая строка - это строка, которая должна быть вставлена в целевую.

Нет, ты не сможешь этого сделать.Вызовы переменных аргументов решаются во время компиляции, и ваш NSArray имеет содержимое только во время выполнения.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top