Dans Cocoa, si je souhaite parcourir une NSMutableArray et supprimer plusieurs objets répondant à certains critères, quel est le meilleur moyen de le faire sans redémarrer la boucle chaque fois que je supprime un objet?
Merci,
Edit: 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);
}
Par souci de clarté, j'aime bien faire une boucle initiale où 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 petits détails de comptabilité.
Édité 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 des tableaux "normaux". Ils parlent, mais ils marchent différemment. Voir 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 qui vous semble la plus claire. 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 est plus claire pour vous, allez-y.
Une autre variation. Donc, vous obtenez 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];
C'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];
}
}
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 des 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'objet. En outre, tout appel à removeObjectAtIndex:
devra copier les valeurs de l'index à la fin du tableau, d'un créneau à la fois.
Plus efficace serait la suivante:
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 des valeurs pendant un redimensionnement. Nous ne modifions pas le tableau en place, nous sommes donc libres d'utiliser Fast Enumeration. Utiliser setArray:
pour remplacer le contenu de array
par itemsToKeep
sera efficace. En fonction de 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 avec un tableau qui ne contient que des noms commençant par b.
[namesArray filterUsingPredicate:caseInsensitiveBNames];
Si vous rencontrez des difficultés pour créer les prédicats dont vous avez besoin, utilisez le lien développeur Apple .
J'ai fait 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é 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 en gros à appeler removeObjectAtIndex: pour chaque élément)
(3) removeObjects:
- 326 ms
(4) faire un nouveau tableau avec des objets qui passent le test - 17 ms
Donc, 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 génération du jeu d’index.
Soit utilisez la boucle en décompte sur les index:
for (NSInteger i = array.count - 1; i >= 0; --i) {
ou faire une copie avec les objets que vous souhaitez conserver.
En particulier, n'utilisez pas de boucle for (id object in array)
ou NSEnumerator
.
Pour iOS 4+ ou OS X 10.6+, Apple a ajouté la série passingTest
d'API dans NSMutableArray
, comme – 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/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--;
}
}
j'espère que cela t'aides...
Une implémentation plus intéressante pourrait consister à utiliser la méthode category 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é.
Un exemple de tableau de dates pour supprimer toutes les dates du passé:
NSMutableArray *dates = ...;
[dates removeObjectsWithPredicate:^BOOL(id obj) {
NSDate *date = (NSDate *)obj;
return [date timeIntervalSinceNow] < 0;
}];
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 d'échanger les éléments que vous souhaitez supprimer avec les éléments 'nth,', n-1th, etc.
Lorsque 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 trouvé, vous pouvez rapidement énumérer 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];
}
}
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 alors être utilisé comme ceci:
[myMutableArray filterUsingTest:^BOOL(id obj, NSUInteger idx) {
return [self doIWantToKeepThisObject:obj atIndex:idx];
}];
la réponse de benzado ci-dessus est ce que vous devez faire pour la performance. Dans l'une de mes applications, removeObjectsInArray prenait 1 minute, mais ajouter à un nouveau tableau prenait 0,023 seconde.
Pendant des années, j'ai préféré itérer à l'envers, 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];