@ "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を作成する方法が見つかりません...
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] ];
}
自動参照カウント(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;
}
私の頭に浮かんだ1つの解決策は、次のような固定された多数の引数で機能するメソッドを作成できることです。
+ (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個を超える '%'文字があるかどうかを確認するチェックを追加して、その場合に例外をスローすることもできます。
@ Chuckは、あなたが NSArrayを可変引数に変換できない であるという事実については正しいです。ただし、文字列でパターン%@
を検索して毎回置き換えることはお勧めしません。 (文字列の途中で文字を置き換えることは、一般に非常に非効率的であり、同じことを別の方法で行うことができる場合は良い考えではありません。)ここで、記述している形式で文字列を作成するより効率的な方法を示します。
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要素配列」メソッドを使用する以外に、新しいプラットフォームが導入されたときに確実に機能するこの問題の解決策はありません。
私が64ビットアーキテクチャでコンパイルするまで、solidsunの答えはうまくいきました。これによりエラーが発生しました:
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;
}
これは単一の要素を持つ配列に対してのみ機能します
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
}
}
Varargsを使用する関数またはメソッドに配列を渡すには 一般的な方法ではありません があります。ただし、この特定のケースでは、次のようなものを使用して偽装できます。
for (NSString *currentReplacement in array)
[string stringByReplacingCharactersInRange:[string rangeOfString:@"%@"]
withString:currentReplacement];
編集:受け入れられた回答はこれを行う方法があると主張していますが、この回答がどれほど壊れやすいように見えても、そのアプローチははるかに壊れやすいです。同じであることが保証されていない実装定義の動作(特に、va_list
の構造)に依存しています。言語とフレームワークの定義された機能のみに依存しているため、私の答えは正しいと私が提案したソリューションはそれほど脆弱ではないと主張します。
はい、可能です。 Mac OS XをターゲットとするGCCでは、少なくともva_list
は単にC配列なので、id
sの1つを作成し、それを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つです。
- (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"];
結果:テキスト
これが可能であると主張しているコードをウェブ上で見つけましたが、私は自分でそれを行うことができませんでしたが、事前に引数の数がわからない場合は、フォーマット文字列を動的に作成する必要があるので、私はしないでくださいポイントがわかりません。
配列を反復して文字列を作成する方がよいでしょう。
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に project を作成しました。また、「stringByReplacingCharactersInRange:」といくつかのテストを使用して、チャックのバージョンを追加しました。
100万個のオブジェクトを配列に使用すると、「stringByReplacingCharactersInRange:」を使用したバージョンは、あまり適切にスケーリングされません(約2分間待ってからアプリを閉じました)。 NSMutableStringのバージョンを使用して、関数は約4秒で文字列を作成しました。テストはシミュレータを使用して行われました。 使用する前に、実際のデバイスでテストを行う必要があります(最低仕様のデバイスを使用してください)。
編集:iPhone 5sでは、NSMutableStringを使用したバージョンは10.471655秒(100万オブジェクト)かかります。 iPhone 5では21.304876秒かかります。