web-dev-qa-db-fra.com

Comment créer une chaîne NSString à partir d'une chaîne de format telle que @ "xxx =% @, yyy =% @" et d'un tableau NSArray d'objets?

Existe-t-il un moyen de créer un nouveau fichier NSString à partir d'une chaîne de formatage telle que @ "xxx =% @, yyy =% @" et d'un tableau NSArray d'objets?

Dans la classe NSSTring, il existe de nombreuses méthodes telles que:

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

mais aucun d'eux ne prend un NSArray en argument, et je ne trouve pas le moyen de créer un va_list à partir d'un NSArray ...

29
Panagiotis Korros

En réalité, il n’est pas difficile de créer une liste va_list à partir d’un tableau NSArray. Voir excellent article de Matt Gallagher sur le sujet.

Voici une catégorie NSString pour faire ce que vous voulez:

@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

Ensuite:

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

Malheureusement, pour le format 64 bits, le format va_list a été modifié. Le code ci-dessus ne fonctionne donc plus. Et ne devrait probablement pas être utilisé de toute façon étant donné que cela dépend du format qui est clairement sujet à changement. Étant donné qu'il n'existe pas de moyen vraiment robuste de créer une liste va_list, une meilleure solution consiste simplement à limiter le nombre d'arguments à un maximum raisonnable (par exemple, 10), puis d'appeler stringWithFormat avec les 10 premiers arguments, comme suit:

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

Basé sur cette réponse en utilisant le comptage automatique de référence (ARC): https://stackoverflow.com/a/8217755/881197

Ajoutez une catégorie à NSString avec la méthode suivante:

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

Une solution qui m'est venue à l'esprit est que je pouvais créer une méthode qui fonctionne avec un grand nombre d'arguments fixe, tel que:

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

Je pourrais aussi ajouter une vérification pour voir si la chaîne de format a plus de 21 caractères '%' et émettre une exception dans ce cas.

16
Panagiotis Korros

@Chuck a raison sur le fait que vous ne pouvez pas convertir un NSArray en varargs . Cependant, je ne recommande pas de rechercher le modèle %@ dans la chaîne et de le remplacer à chaque fois. (Remplacer des caractères au milieu d'une chaîne est généralement assez inefficace et ce n'est pas une bonne idée si vous pouvez accomplir la même chose d'une manière différente.) Voici un moyen plus efficace de créer une chaîne au format que vous décrivez:

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

J'ai inclus le pool autorelease pour une bonne gestion, car une chaîne autoreleased sera créée pour chaque entrée de tableau, et le tableau mutable est également autoreleased. Vous pouvez facilement en faire une méthode/fonction et renvoyer composedString sans la conserver, et gérer la libération automatique ailleurs dans le code si vous le souhaitez.

4
Quinn Taylor

Cette réponse est boguée. Comme indiqué, il n’existe pas de solution à ce problème qui fonctionnera avec l’introduction de nouvelles plates-formes autre que l’utilisation de la méthode "tableau à 10 éléments".


La réponse de solidsun fonctionnait bien jusqu'à ce que je compile avec une architecture 64 bits. Cela a provoqué une erreur: 

EXC_BAD_ADDRESS type EXC_I386_GPFLT

La solution consistait à utiliser une approche légèrement différente pour passer la liste des arguments à la méthode:

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

Ceci ne fonctionne que pour les tableaux avec un seul élément

4
Brett

Pour ceux qui ont besoin d’une solution Swift, voici une extension pour le faire dans 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
Bocaxica

Il il n'y a pas de moyen général } de passer un tableau à une fonction ou à une méthode utilisant varargs. Dans ce cas particulier, toutefois, vous pouvez le simuler en utilisant quelque chose comme:

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

EDIT: La réponse acceptée affirme qu'il existe un moyen de le faire, mais quelle que soit la fragilité de cette réponse, cette approche est beaucoup plus fragile. Il repose sur un comportement défini par l'implémentation (en particulier la structure d'un va_list) qui n'est pas garanti pour rester identique. Je maintiens que ma réponse est correcte et que la solution proposée est moins fragile, car elle ne repose que sur des caractéristiques définies du langage et des frameworks.

3
Chuck

Oui c'est possible. Au moins dans GCC ciblant Mac OS X, va_list est simplement un tableau C, vous devez donc choisir l'un des ids, puis indiquer à NSArray de le remplir:

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

Vous ne devriez pas compter sur cette nature dans un code qui devrait être portable. Développeurs iPhone, c’est une chose que vous devriez certainement tester sur l’appareil.

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

Testé avec le format et les arguments:

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

Résultat: xxx = XXX, yyy = YYY dernier composant

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

Résultat: xxx = XXX, yyy = YYY dernier composant

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

Résultat: xxx = XXX dernier composant

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

Résultat: dernier composant

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

Résultat: du texte

1
Ruslan Soldatenko

J'ai trouvé sur le Web un code qui prétend que c'est possible, mais je n'ai pas réussi à le faire moi-même. Toutefois, si vous ne connaissez pas le nombre d'arguments à l'avance, vous devez également créer la chaîne de format de façon dynamique. 'vois pas le point. 

Vous feriez mieux de simplement construire la chaîne en itérant le tableau.

Vous pouvez trouver la méthode instance de stringByAppendingString: ou stringByAppendingFormat:.

0
Ron Srebro

On peut créer une catégorie pour NSString et créer une fonction qui reçoit un format, un tableau et renvoie la chaîne avec les objets remplacés.

@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

Comme NSArray est créé à l'exécution, nous ne pouvons pas fournir d'avertissements au moment de la compilation, mais nous pouvons utiliser NSAssert pour nous indiquer si le nombre de spécificateurs est égal au nombre d'objets dans le tableau.

Créé un projet sur Github où cette catégorie peut être trouvée. Également ajouté la version de Chuck en utilisant 'stringByReplacingCharactersInRange:' plus quelques tests. 

En utilisant un million d'objets dans un tableau, la version avec 'stringByReplacingCharactersInRange:' ne s'adapte pas très bien (a attendu environ 2 minutes puis a fermé l'application). En utilisant la version avec NSMutableString, la fonction a créé la chaîne en environ 4 secondes. Les tests ont été réalisés à l'aide d'un simulateur. Avant utilisation, les tests doivent être effectués sur un périphérique réel (utilisez un périphérique avec les spécifications les plus basses).

Edit: sur iPhone 5s, la version avec NSMutableString nécessite 10.471655s (un million d’objets); sur iPhone 5 prend 21.304876s.

0
Athan