Question

Dans Cocoa, si je souhaite parcourir en boucle un NSMutableArray et supprimer plusieurs objets correspondant à certains critères, quel est le meilleur moyen de le faire sans redémarrer la boucle à chaque fois que je supprime un objet?

Merci,

Modifier: Juste pour préciser - je cherchais le meilleur moyen, par exemple quelque chose de plus élégant que de mettre à jour manuellement l'index auquel je suis. Par exemple, en C ++, je peux le faire;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
Était-ce utile?

La solution

Par souci de clarté, j’aime créer une boucle initiale dans laquelle je collecte les éléments à supprimer. Puis je les supprime. Voici un exemple utilisant la syntaxe Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

for (SomeObjectClass *item in originalArrayOfItems) {
    if ([item shouldBeDiscarded])
        [discardedItems addObject:item];
}

[originalArrayOfItems removeObjectsInArray:discardedItems];

Ensuite, il n’est plus question de savoir si les index sont mis à jour correctement ou d’autres détails de comptabilité.

Modifié pour ajouter:

Il a été noté dans d'autres réponses que la formulation inverse devrait être plus rapide. Par exemple, si vous parcourez le tableau et composez un nouveau tableau d'objets à conserver, au lieu d'objets à ignorer. C’est peut-être vrai (bien qu’en est-il des coûts de mémoire et de traitement liés à l’allocation d’un nouveau tableau et à l’élimination de l’ancien?), Mais même si c’est plus rapide, cela risque de ne pas être aussi important que pour une implémentation naïve, car NSArrays ne vous comportez pas comme " normal " tableaux. Ils parlent, mais ils marchent différemment. Consultez une bonne analyse ici:

La formulation inverse peut être plus rapide, mais je n’ai jamais eu besoin de me demander si c’est le cas, car la formulation ci-dessus a toujours été assez rapide pour mes besoins.

Pour moi, le message à retenir est d'utiliser la formulation la plus claire pour vous. Optimiser uniquement si nécessaire. Personnellement, je trouve la formulation ci-dessus la plus claire, c'est pourquoi je l'utilise. Mais si la formulation inverse vous semble plus claire, foncez.

Autres conseils

Une autre variante. Vous obtenez ainsi une lisibilité et de bonnes performances:

NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet];
SomeObjectClass *item;
NSUInteger index = 0;

for (item in originalArrayOfItems) {
    if ([item shouldBeDiscarded])
        [discardedItems addIndex:index];
    index++;
}

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

Ceci est un problème très simple. Vous venez d'itérer à l'envers:

for (NSInteger i = array.count - 1; i >= 0; i--) {
   ElementType* element = array[i];
   if ([element shouldBeRemoved]) {
       [array removeObjectAtIndex:i];
   }
}

C’est un schéma très courant.

Certaines des autres réponses auraient de mauvaises performances sur les très grands tableaux, car des méthodes telles que removeObject: et removeObjectsInArray: impliquent une recherche linéaire du destinataire, qui est: une perte parce que vous savez déjà où se trouve l'objet. De même, tout appel à removeObjectAtIndex: devra copier les valeurs de l'index à la fin du tableau, d'un créneau à la fois.

Plus efficace serait le suivant:

NSMutableArray *array = ...
NSMutableArray *itemsToKeep = [NSMutableArray arrayWithCapacity:[array count]];
for (id object in array) {
    if (! shouldRemove(object)) {
        [itemsToKeep addObject:object];
    }
}
[array setArray:itemsToKeep];

Etant donné que nous définissons la capacité de itemsToKeep , nous ne perdons pas de temps à copier des valeurs pendant un redimensionnement. Nous ne modifions pas le tableau en place, nous sommes donc libres d'utiliser Fast Enumeration. L'utilisation de setArray: pour remplacer le contenu de tableau par itemsToKeep sera efficace. Selon votre code, vous pouvez même remplacer la dernière ligne par:

[array release];
array = [itemsToKeep retain];

Donc, il n'est même pas nécessaire de copier des valeurs, il suffit d'échanger un pointeur.

Vous pouvez utiliser NSpredicate pour supprimer des éléments de votre tableau mutable. Cela nécessite non pour les boucles.

Par exemple, si vous avez un NSMutableArray de noms, vous pouvez créer un prédicat comme celui-ci:

NSPredicate *caseInsensitiveBNames = 
[NSPredicate predicateWithFormat:@"SELF beginswith[c] 'b'"];

La ligne suivante vous laissera un tableau contenant uniquement des noms commençant par b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Si vous ne parvenez pas à créer les prédicats dont vous avez besoin, utilisez cette développeur Apple. lien .

J'ai effectué un test de performance en utilisant 4 méthodes différentes. Chaque test a itéré à travers tous les éléments d'un tableau de 100 000 éléments et supprimé tous les 5 éléments. Les résultats n'ont pas beaucoup varié avec / sans optimisation. Celles-ci ont été effectuées sur un iPad 4:

(1) removeObjectAtIndex: - 271 ms

.

(2) removeObjectsAtIndexes: - 1010 ms (car la construction de l'ensemble d'index prend environ 700 ms; sinon, cela revient à appeler removeObjectAtIndex: pour chaque élément )

(3) removeObjects: - 326 ms

.

(4) créer un nouveau tableau avec des objets ayant réussi le test - 17 ms

Donc, créer un nouveau tableau est de loin le plus rapide. Les autres méthodes sont toutes comparables, excepté que l’utilisation de removeObjectsAtIndexes: sera pire avec plus d’éléments à supprimer, en raison du temps nécessaire à la génération de l’index.

Utilisez le décompte de boucle sur les index:

for (NSInteger i = array.count - 1; i >= 0; --i) {

ou faites une copie avec les objets que vous souhaitez conserver.

En particulier, n'utilisez pas de boucle pour (id objet dans un tableau) ou NSEnumerator .

Pour iOS 4+ ou OS X 10.6+, Apple a ajouté une série passantes dans NSMutableArray , comme & # 8211; indexesOfObjectsPassingTest: . Une solution avec une telle API serait:

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];

De nos jours, vous pouvez utiliser une énumération par blocs inversés. Un exemple de code simple:

NSMutableArray *array = [@[@{@"name": @"a", @"shouldDelete": @(YES)},
                           @{@"name": @"b", @"shouldDelete": @(NO)},
                           @{@"name": @"c", @"shouldDelete": @(YES)},
                           @{@"name": @"d", @"shouldDelete": @(NO)}] mutableCopy];

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    if([obj[@"shouldDelete"] boolValue])
        [array removeObjectAtIndex:idx];
}];

Résultat:

(
    {
        name = b;
        shouldDelete = 0;
    },
    {
        name = d;
        shouldDelete = 0;
    }
)

une autre option avec une seule ligne de code:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];

De manière plus déclarative, en fonction des critères correspondant aux éléments à supprimer, vous pouvez utiliser:

[theArray filterUsingPredicate:aPredicate]

@Nathan devrait être très efficace

Voici le moyen facile et propre. J'aime dupliquer mon tableau directement dans l'appel d'énumération rapide:

for (LineItem *item in [NSArray arrayWithArray:self.lineItems]) 
{
    if ([item.toBeRemoved boolValue] == YES) 
    {
        [self.lineItems removeObject:item];
    }
}

De cette façon, vous énumérez une copie du tableau à supprimer, les deux contenant les mêmes objets. Un tableau NSArray contient uniquement des pointeurs d’objets, c’est donc une solution parfaite en termes de mémoire et de performances.

Ajoutez les objets que vous souhaitez supprimer à un deuxième tableau et, après la boucle, utilisez -removeObjectsInArray:.

cela devrait le faire:

    NSMutableArray* myArray = ....;

    int i;
    for(i=0; i<[myArray count]; i++) {
        id element = [myArray objectAtIndex:i];
        if(element == ...) {
            [myArray removeObjectAtIndex:i];
            i--;
        }
    }

espérons que cela aide ...

Pourquoi n'ajoutez-vous pas les objets à supprimer à un autre NSMutableArray. Lorsque vous avez terminé votre itération, vous pouvez supprimer les objets que vous avez collectés.

Que diriez-vous de permuter les éléments que vous souhaitez supprimer avec les éléments 'nth,', n-1, etc.?

Une fois que vous avez terminé, vous redimensionnez le tableau en "taille précédente - nombre de swaps"

Si tous les objets de votre tableau sont uniques ou si vous souhaitez supprimer toutes les occurrences d'un objet lorsqu'il est trouvé, vous pouvez énumérer rapidement une copie de tableau et utiliser [NSMutableArray removeObject:] pour supprimer l'objet de l'original.

NSMutableArray *myArray;
NSArray *myArrayCopy = [NSArray arrayWithArray:myArray];

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}

La réponse de benzado ci-dessus est ce que vous devez faire pour améliorer les performances. Dans l'une de mes applications, removeObjectsInArray prenait 1 minute, mais ajouter à un nouveau tableau prenait 0,023 seconde.

Je définis une catégorie qui me permet de filtrer à l'aide d'un bloc, comme ceci:

@implementation NSMutableArray (Filtering)

- (void)filterUsingTest:(BOOL (^)(id obj, NSUInteger idx))predicate {
    NSMutableIndexSet *indexesFailingTest = [[NSMutableIndexSet alloc] init];

    NSUInteger index = 0;
    for (id object in self) {
        if (!predicate(object, index)) {
            [indexesFailingTest addIndex:index];
        }
        ++index;
    }
    [self removeObjectsAtIndexes:indexesFailingTest];

    [indexesFailingTest release];
}

@end

qui peut ensuite être utilisé comme ceci:

[myMutableArray filterUsingTest:^BOOL(id obj, NSUInteger idx) {
    return [self doIWantToKeepThisObject:obj atIndex:idx];
}];

Une meilleure implémentation pourrait consister à utiliser la méthode de la catégorie ci-dessous sur NSMutableArray.

@implementation NSMutableArray(BMCommons)

- (void)removeObjectsWithPredicate:(BOOL (^)(id obj))predicate {
    if (predicate != nil) {
        NSMutableArray *newArray = [[NSMutableArray alloc] initWithCapacity:self.count];
        for (id obj in self) {
            BOOL shouldRemove = predicate(obj);
            if (!shouldRemove) {
                [newArray addObject:obj];
            }
        }
        [self setArray:newArray];
    }
}

@end

Le bloc de prédicat peut être implémenté pour effectuer un traitement sur chaque objet du tableau. Si le prédicat renvoie true, l'objet est supprimé.

Exemple de tableau de dates pour supprimer toutes les dates antérieures:

NSMutableArray *dates = ...;
[dates removeObjectsWithPredicate:^BOOL(id obj) {
    NSDate *date = (NSDate *)obj;
    return [date timeIntervalSinceNow] < 0;
}];

Itérer à l'envers était mon préféré depuis des années, mais pendant longtemps je n'ai jamais rencontré le cas où l'objet 'le plus profond' (le plus grand nombre) était supprimé en premier. Avant que le pointeur ne passe à l'index suivant, il n'y a rien et il se bloque.

La manière de Benzado est la plus proche de ce que je fais maintenant mais je n’ai jamais réalisé qu’il y aurait un remaniement de la pile après chaque suppression.

sous Xcode 6 cela fonctionne

NSMutableArray *itemsToKeep = [NSMutableArray arrayWithCapacity:[array count]];

    for (id object in array)
    {
        if ( [object isNotEqualTo:@"whatever"]) {
           [itemsToKeep addObject:object ];
        }
    }
    array = nil;
    array = [[NSMutableArray alloc]initWithArray:itemsToKeep];
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top