32

有没有办法从像 @"xxx=%@, yyy=%@" 这样的格式字符串和对象的 NSArray 中创建一个新的 NSString?

在 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 作为参数,我找不到从 NSArray 创建 va_list 的方法......

4

13 回答 13

46

从 NSArray 创建 va_list 实际上并不难。请参阅 Matt Gallagher 关于该主题的优秀文章

这是一个 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),然后使用前 10 个参数调用 stringWithFormat,如下所示:

+ (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] ];
}
于 2009-06-30T04:36:10.843 回答
37

基于使用自动引用计数(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;
}
于 2012-11-21T18:28:16.390 回答
16

我想到的一个解决方案是,我可以创建一种使用固定的大量参数的方法,例如:

+ (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 个 '%' 字符,并在这种情况下引发异常。

于 2009-06-29T22:28:00.977 回答
4

@Chuck关于您无法将 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而不保留它,并在需要时在代码的其他位置处理自动释放。

于 2009-06-29T16:37:32.903 回答
4

这个答案是错误的。如前所述,除了使用“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;
}

这仅适用于具有单个元素的数组

于 2014-01-28T23:59:46.023 回答
3

没有通用方法可以将数组传递给使用可变参数的函数或方法。但是,在这种特殊情况下,您可以使用以下方法伪造它:

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

编辑:接受的答案声称有一种方法可以做到这一点,但不管这个答案看起来多么脆弱,这种方法要脆弱得多。它依赖于va_list不保证保持不变的实现定义的行为(特别是 a 的结构)。我坚持认为我的答案是正确的,并且我提出的解决方案不那么脆弱,因为它只依赖于语言和框架的定义特征。

于 2009-06-29T15:50:35.167 回答
3

对于那些需要 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
    }

}
于 2015-08-30T08:11:38.047 回答
2

是的,有可能。在针对 Mac OS X 的 GCC 中,至少,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 开发人员,这是您绝对应该在设备上测试的一件事。

于 2009-06-30T03:35:35.207 回答
1
- (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"];

结果:一些文本

于 2016-11-16T17:26:13.313 回答
0

我在网上找到了一些声称这是可能的代码,但是我自己没有设法做到这一点,但是如果您事先不知道参数的数量,您还需要动态构建格式字符串,所以我只是不没看到重点。

你最好只通过迭代数组来构建字符串。

您可能会发现 stringByAppendingString: 或 stringByAppendingFormat: 实例方法很方便。

于 2009-06-29T15:31:12.223 回答
0

可以为 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:' 和一些测试添加了 Chuck 的版本。

在数组中使用一百万个对象,带有“stringByReplacingCharactersInRange:”的版本不能很好地扩展(等待大约 2 分钟然后关闭应用程序)。使用带有 NSMutableString 的版本,函数在大约 4 秒内生成字符串。测试是使用模拟器进行的。使用前,应在真实设备上进行测试(使用最低规格的设备)。

编辑:在 iPhone 5s 上,带有 NSMutableString 的版本需要 10.471655s(一百万个对象);在 iPhone 5 上需要 21.304876 秒。

于 2015-06-20T10:18:52.420 回答
-2

这是没有明确创建数组的答案:

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

第一个字符串是要格式化的目标字符串,下一个字符串是要插入目标的字符串。

于 2013-06-26T04:53:07.047 回答
-3

不,你不能。变量参数调用在编译时解决,您的 NSArray 仅在运行时有内容。

于 2009-06-29T15:39:14.423 回答