Meilleure façon de supprimer NSMutableArray lors de l’itération?

Dans Cocoa, si je souhaite parcourir un NSMutableArray et supprimer plusieurs objects correspondant à un certain critère, quelle est la meilleure façon de procéder sans redémarrer la boucle chaque fois que je supprime un object?

Merci,

Edit: Juste pour clarifier – 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); } 

Pour plus de clarté, j’aime faire une boucle initiale où je recueille les éléments à supprimer. Ensuite, 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 pas question de savoir si les index sont mis à jour correctement ou d’autres petits détails comptables.

Edité pour append:

Il a été noté dans d’autres réponses que la formulation inverse devrait être plus rapide. C’est-à-dire si vous parcourez le tableau et composez un nouveau tableau d’objects à conserver, au lieu d’objects à éliminer. Cela peut être vrai (mais qu’en est-il de la mémoire et des coûts de traitement liés à l’allocation d’un nouveau tableau et à l’élimination de l’ancien?), Mais même s’il est plus rapide que pour une implémentation naïve, NSArrays ne vous comportez pas comme des tableaux “normaux”. Ils parlent, mais ils marchent d’une manière différente. Voir une bonne parsing ici:

La formulation inverse peut être plus rapide, mais je n’ai jamais eu à me soucier de savoir 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 plus claire, c’est pourquoi je l’utilise. Mais si la formulation inverse est plus claire pour vous, allez-y.

Une variation de plus. Donc, vous obtenez une lisibilité et une bonne performance:

 NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet]; SomeObjectClass *item; NSUInteger index = 0; for (item in originalArrayOfItems) { if ([item shouldBeDiscarded]) [discardedItems addIndex:index]; index++; } [originalArrayOfItems removeObjectsAtIndexes:discardedItems]; 

C’est un problème très simple. Vous ne faites qu’itérer les retours en arrière:

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

Ceci est un modèle très commun.

Certaines des autres réponses auraient des performances médiocres sur les très grands tableaux, car les méthodes telles que removeObject: et removeObjectsInArray: impliquent une recherche linéaire du récepteur, ce qui est une perte car vous savez déjà où se trouve l’object. De plus, tout appel à removeObjectAtIndex: devra copier les valeurs de l’index à la fin du tableau par un emplacement à 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]; 

Comme nous définissons la capacité de itemsToKeep , nous ne perdons pas de temps à copier les valeurs lors d’un redimensionnement. Nous ne modifions pas le tableau en place, nous sums donc libres d’utiliser Fast Enumeration. Utiliser setArray: pour remplacer le contenu du array 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’y a même pas besoin de copier des valeurs, échangez seulement un pointeur.

Vous pouvez utiliser NSpredicate pour supprimer des éléments de votre tableau mutable. Cela ne nécessite pas de 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 ce lien développeur Apple .

Soit utiliser la boucle en comptant sur les index:

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

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

En particulier, n’utilisez pas de boucle for (id object in array) ou NSEnumerator .

J’ai fait un test de performance en utilisant 4 méthodes différentes. Chaque test a été répété sur tous les éléments d’un tableau de 100 000 éléments et a supprimé tous les 5 éléments. Les résultats n’ont pas beaucoup varié avec / sans optimisation. Celles-ci ont été réalisé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 objects passant le test – 17 ms

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

Pour iOS 4+ ou OS X 10.6+, Apple a ajouté des séries de passingTest API dans NSMutableArray , telles que – 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 l’é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 la manière simple et propre. J’aime dupliquer mon tableau dans l’appel à é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 en cours de suppression, tous deux contenant les mêmes objects. Un NSArray ne contient que des pointeurs d’object, ce qui évite les problèmes de mémoire et de performances.

Ajoutez les objects à 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--; } } 

J'espère que cela t'aides...

Pourquoi n’ajoutez-vous pas les objects à supprimer à un autre NSMutableArray? Une fois l’itération terminée, vous pouvez supprimer les objects que vous avez collectés.

Que diriez-vous d’échanger les éléments que vous voulez supprimer avec le ‘nième élément’, le n-ème élément et ainsi de suite?

Lorsque vous avez terminé, vous redimensionnez le tableau en “taille précédente – nombre de swaps”

Si tous les objects de votre tableau sont uniques ou que vous souhaitez supprimer toutes les occurrences d’un object lorsque vous les trouvez, vous pouvez les énumérer rapidement et utiliser [NSMutableArray removeObject:] pour supprimer l’object 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 devriez faire pour la préformé. Dans l’une de mes applications, removeObjectsInArray a duré 1 minute, l’ajout de seulement 0,023 seconde à un nouveau tableau.

Je définis une catégorie qui me permet de filtrer en utilisant 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 alors être utilisé comme ceci:

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

Une meilleure implémentation pourrait être d’utiliser la méthode de 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 le traitement sur chaque object du tableau. Si le prédicat renvoie true, l’object est supprimé.

Un exemple de tableau de dates permettant de supprimer toutes les dates antérieures:

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

Ce qui me plaisait le plus depuis des années, c’était de parcourir le passé, mais pendant longtemps je n’ai jamais rencontré le cas où l’object «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 stack 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];