wake-up-neo.com

Wie erstelle ich einen NSString aus einem Formatstring wie @ "xxx =% @, yyy =% @" und einem NSArray von Objekten?

Gibt es eine Möglichkeit zum Erstellen eines neuen NSStrings aus einer Formatzeichenfolge wie @ "xxx =% @, yyy =% @" und einem NSArray von Objekten?

In der NSSTring-Klasse gibt es viele Methoden wie:

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

aber keiner von ihnen nimmt ein NSArray als Argument an, und ich kann keinen Weg finden, eine Va_list aus einem NSArray zu erstellen ...

29

Es ist tatsächlich nicht schwer, eine va_list aus einem NSArray zu erstellen. Siehe Matt Gallaghers exzellenter Artikel zum Thema.

Hier ist eine NSString-Kategorie, um das zu tun, was Sie wollen:

@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

Dann:

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

Leider hat sich das va_list-Format für 64-Bit geändert, sodass der obige Code nicht mehr funktioniert. Und sollte wahrscheinlich sowieso nicht verwendet werden, da es von dem Format abhängt, das eindeutig geändert werden kann. Da es keine wirklich robuste Methode gibt, eine va_list zu erstellen, ist es eine bessere Lösung, die Anzahl der Argumente auf ein vernünftiges Maximum zu begrenzen (z. B. 10) und dann stringWithFormat mit den ersten 10 Argumenten aufzurufen.

+ (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] ];
}
46
Peter N Lewis

Basierend auf dieser Antwort unter Verwendung von Automatic Reference Counting (ARC): https://stackoverflow.com/a/8217755/881197

Fügen Sie eine Kategorie zu NSString mit der folgenden Methode hinzu:

+ (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;
}
35
SolidSun

Eine Lösung, die mir in den Sinn kam, ist, dass ich eine Methode erstellen könnte, die mit einer festen Anzahl von Argumenten arbeitet, wie:

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

Ich könnte auch eine Prüfung hinzufügen, um zu sehen, ob die Formatzeichenfolge mehr als 21 '%' Zeichen enthält, und in diesem Fall eine Ausnahme auslösen.

16

@Chuck stimmt mit der Tatsache, dass Sie einen NSArray nicht in Varargs konvertieren können. Ich empfehle jedoch nicht, nach dem Muster %@ in der Zeichenfolge zu suchen und es jedes Mal zu ersetzen. (Das Ersetzen von Zeichen in der Mitte eines Strings ist in der Regel ziemlich ineffizient und keine gute Idee, wenn Sie dasselbe auf andere Weise erreichen können.) Hier ist eine effizientere Methode zum Erstellen eines Strings mit dem von Ihnen beschriebenen Format:

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

Ich habe den Autorelease-Pool für ein gutes Housekeeping hinzugefügt, da für jeden Array-Eintrag eine automatisch gelöste Zeichenfolge erstellt wird und das veränderliche Array ebenfalls autoreleased ist. Sie können dies leicht in eine Methode/Funktion umwandeln und composedString zurückgeben, ohne es beizubehalten, und die Autorelease an anderer Stelle im Code ausführen, falls dies gewünscht wird.

4
Quinn Taylor

Diese Antwort ist fehlerhaft. Wie bereits erwähnt, gibt es keine Lösung für dieses Problem, die garantiert funktioniert, wenn neue Plattformen eingeführt werden, mit Ausnahme der Methode "10 Elemente".


Die Antwort von Solidsun funktionierte gut, bis ich mit der 64-Bit-Architektur kompilierte. Dies hat einen Fehler verursacht: 

EXC_BAD_ADDRESS-Typ EXC_I386_GPFLT

Die Lösung bestand darin, die Argumentliste an die Methode etwas weiterzugeben:

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

Dies funktioniert nur für Arrays mit einem einzelnen Element.

4
Brett

Für diejenigen, die eine Swift-Lösung benötigen, ist hier eine Erweiterung dazu in Swift verfügbar

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
Bocaxica

Ist kein allgemeiner Weg , um ein Array an eine Funktion oder Methode zu übergeben, die varargs verwendet. In diesem speziellen Fall können Sie es jedoch fälschen, indem Sie Folgendes verwenden:

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

BEARBEITEN: Die akzeptierte Antwort behauptet, dass es einen Weg gibt, dies zu tun, aber unabhängig davon, wie fragil diese Antwort erscheinen mag, ist dieser Ansatz viel fragiler. Es basiert auf implementierungsdefiniertem Verhalten (insbesondere der Struktur eines va_list), das nicht garantiert gleich bleibt. Ich behaupte, dass meine Antwort richtig ist und meine vorgeschlagene Lösung weniger anfällig ist, da sie nur auf definierten Merkmalen der Sprache und der Rahmenbedingungen beruht.

3
Chuck

Ja, es ist möglich. In Mac OS X für GCC ist zumindest va_list einfach ein C-Array. Daher müssen Sie eines von ids erstellen und dem NSArray mitteilen, dass es es ausfüllen soll:

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);

Sie sollten sich nicht auf diese Art in Code verlassen, der portabel sein sollte. iPhone-Entwickler, dies ist eine Sache, die Sie unbedingt auf dem Gerät testen sollten.

2
Peter Hosey
- (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;
}

Getestet mit Format und Argumenten:

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

Ergebnis: xxx = XXX, yyy = YYY letzte Komponente

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

Ergebnis: xxx = XXX, yyy = YYY letzte Komponente

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

Ergebnis: xxx = XXX letzte Komponente

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

Ergebnis: letzte Komponente

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

Ergebnis: etwas Text

1

Ich habe im Internet einen Code gefunden, der behauptet, dass dies möglich ist. Ich habe es jedoch nicht selbst geschafft. Wenn Sie jedoch nicht die Anzahl der Argumente im Voraus kennen, müssen Sie den Formatstring auch dynamisch erstellen, sodass ich ihn einfach nicht baue den Punkt nicht sehen. 

Es ist besser, wenn Sie die Zeichenfolge erstellen, indem Sie das Array durchlaufen.

Die Instanzmethode stringByAppendingString: oder stringByAppendingFormat: ist möglicherweise praktisch.

0
Ron Srebro

Man kann eine Kategorie für NSString erstellen und eine Funktion erstellen, die ein Format und ein Array empfängt und die Zeichenfolge mit ersetzten Objekten zurückgibt.

@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

Da NSArray zur Laufzeit erstellt wird, können wir keine Warnungen zum Zeitpunkt der Kompilierung bereitstellen. Mit NSAssert können Sie uns jedoch mitteilen, ob die Anzahl der Bezeichner der Anzahl der Objekte im Array entspricht.

Erstellt ein Projekt auf Github, wo diese Kategorie zu finden ist. Außerdem wurde die Version von Chuck hinzugefügt, indem 'stringByReplacingCharactersInRange:' sowie einige Tests verwendet wurden. 

Bei Verwendung einer Million Objekte in einem Array wird die Version mit 'stringByReplacingCharactersInRange:' nicht sehr gut skaliert (etwa 2 Minuten gewartet und dann die App geschlossen). Bei Verwendung der Version mit NSMutableString hat die Funktion die Zeichenfolge in etwa 4 Sekunden erstellt. Die Tests wurden mit einem Simulator durchgeführt. Vor dem Einsatz sollten Tests auf einem realen Gerät durchgeführt werden (verwenden Sie ein Gerät mit den niedrigsten Spezifikationen).

Edit: Auf dem iPhone 5s dauert die Version mit NSMutableString 10.471655s (eine Million Objekte); auf dem iPhone 5 dauert es 21.304876s.

0
Athan