La meilleure façon de supprimer de NSMutableArray pendant l'itération?

194

Dans Cocoa, si je souhaite parcourir un NSMutableArray et supprimer plusieurs objets qui répondent à certains critères, quelle est la meilleure façon de procéder sans redémarrer la boucle chaque fois que je supprime un objet?

Merci,

Edit: Juste pour clarifier - je cherchais la meilleure façon, par exemple quelque chose de plus élégant que de mettre à jour manuellement l'index où je suis. Par exemple en C ++ je peux faire;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
Andrew Grant
la source
9
Boucle de l'arrière vers l'avant.
Hot Licks
Personne ne répond au "POURQUOI"
onmyway133
1
@HotLicks Un de mes favoris de tous les temps et la solution la plus sous-estimée en programmation en général: D
Julian F. Weinert

Réponses:

388

Pour plus de clarté, j'aime faire une boucle initiale où je collecte 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 indices sont mis à jour correctement ou d'autres petits détails de comptabilité.

Modifié pour ajouter:

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'objets à conserver, au lieu d'objets à jeter. Cela peut être vrai (mais qu'en est-il du coût de mémoire et de traitement pour allouer un nouveau tableau et éliminer l'ancien?), Mais même s'il est plus rapide, ce n'est peut-être pas aussi grave que pour une implémentation naïve, car NSArrays ne se comportent 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 m'en soucier, 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. Optimisez 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.

Christopher Ashworth
la source
47
Attention, cela pourrait créer des bugs si les objets sont plusieurs fois dans un tableau. Comme alternative, vous pouvez utiliser un NSMutableIndexSet et - (void) removeObjectsAtIndexes.
Georg Schölly
1
Il est en fait plus rapide de faire l'inverse. La différence de performances est assez énorme, mais si votre baie n'est pas si grande, les temps vont être triviaux dans les deux cas.
user1032657
J'ai utilisé inverser ... rendu le tableau comme nul .. puis seulement ajouté ce que je voulais et recharger les données ... fait ... créé dummyArray qui est le miroir du tableau principal afin que nous ayons les données réelles principales
Fahim Parkar
La mémoire supplémentaire doit être allouée pour faire l'inverse. Ce n'est pas un algorithme sur place et ce n'est pas une bonne idée dans certains cas. Lorsque vous supprimez directement, vous déplacez simplement le tableau (ce qui est très efficace car il ne se déplacera pas un par un, il peut déplacer un gros morceau), mais lorsque vous créez un nouveau tableau, vous avez besoin de toutes les allocations et affectations. Donc, si vous ne supprimez que quelques éléments, l'inverse sera beaucoup plus lent. C'est un algorithme typique qui semble juste.
jack
82

Encore une variante. Ainsi, 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];
Corey Floyd
la source
C'est génial. J'essayais de supprimer plusieurs éléments du tableau et cela a parfaitement fonctionné. D'autres méthodes causaient des problèmes :) Merci mec
AbhinavVinay
Corey, cette réponse (ci-dessous) a dit que, removeObjectsAtIndexesest la pire méthode pour retirer les objets, êtes-vous d'accord avec cela? Je pose cette question parce que votre réponse est trop ancienne maintenant. Encore faut-il choisir le meilleur?
Hemang
J'aime beaucoup cette solution en termes de lisibilité. Comment est-il en termes de performances par rapport à la réponse @HotLicks?
Victor Maia Aldecôa du
Cette méthode doit être définitivement encouragée lors de la suppression d'objets du tableau dans Obj-C.
boreas
enumerateObjectsUsingBlock:vous obtiendrait l'incrément d'index gratuitement.
pkamb
42

Il s'agit d'un problème très simple. Vous venez d'itérer en arrière:

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

Il s'agit d'un schéma très courant.

Hot Licks
la source
aurait manqué la réponse de Jens, car il n'a pas écrit le code: P .. thnx
m8
3
C'est la meilleure méthode. Il est important de se rappeler que la variable d'itération doit être signée, bien que les indices de tableau dans Objective-C soient déclarés non signés (je peux imaginer à quel point Apple regrette cela maintenant).
mojuba
39

Certaines des autres réponses auraient de mauvaises performances sur de très grands tableaux, car les méthodes comme removeObject:et removeObjectsInArray:impliquent de faire une recherche linéaire du récepteur, ce qui est un gaspillage parce que vous savez déjà où se trouve l'objet. Aussi, tout appel àremoveObjectAtIndex: devra copier les valeurs de l'index à la fin du tableau vers le haut d'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];

Parce 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 l'énumération rapide. Utiliser setArray:pour remplacer le contenu de arrayavec itemsToKeepsera efficace. Selon votre code, vous pouvez même remplacer la dernière ligne par:

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

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

benzado
la source
Cet algo s'avère bon en termes de complexité temporelle. J'essaie de le comprendre en termes de complexité de l'espace. J'ai la même implémentation qui me permet de définir la capacité de «itemsToKeep» avec le «nombre de tableaux» réel. Mais disons que je ne veux pas quelques objets du tableau donc je ne les ajoute pas à itemsToKeep. La capacité de mes itemsToKeep est donc de 10, mais je ne stocke en fait que 6 objets. Est-ce à dire que je gaspille de l'espace de 4 objets? PS ne trouve pas de défauts, j'essaie juste de comprendre la complexité de l'algo. :)
tech_human
Oui, vous disposez d'un espace réservé pour quatre pointeurs que vous n'utilisez pas. Gardez à l'esprit que le tableau ne contient que des pointeurs, pas les objets eux-mêmes, donc pour quatre objets, cela signifie 16 octets (architecture 32 bits) ou 32 octets (64 bits).
benzado
Notez que l'une des solutions va au mieux nécessiter 0 espace supplémentaire (vous supprimez les éléments en place) ou au pire doubler la taille du tableau d'origine (car vous faites une copie du tableau). Encore une fois, car nous avons affaire à des pointeurs et non à des copies complètes d'objets, c'est assez bon marché dans la plupart des circonstances.
benzado
OK, j'ai compris. Merci Benzado !!
tech_human
Selon cet article, le conseil de la méthode arrayWithCapacity: sur la taille du tableau n'est pas réellement utilisé.
Kremk
28

Vous pouvez utiliser NSpredicate pour supprimer des éléments de votre tableau mutable. Cela ne nécessite aucune boucle.

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 qui ne contient que 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 .


la source
3
Ne nécessite aucun visible pour les boucles. Il y a une boucle dans l'opération de filtrage, vous ne la voyez tout simplement pas.
Hot Licks
18

J'ai fait un test de performance en utilisant 4 méthodes différentes. Chaque test a parcouru 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. Cela a été fait sur un iPad 4:

(1) removeObjectAtIndex: - 271 ms

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

(3) removeObjects: - 326 ms

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

La création d'un nouveau tableau est donc de loin la 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 pour construire l'ensemble d'index.

user1032657
la source
1
Avez-vous compté le temps de créer un nouveau tableau et de le désallouer ensuite. Je ne pense pas qu'il puisse le faire seul en 17 ms. Plus 75 000 affectations?
jack
Le temps nécessaire pour créer et désallouer un nouveau tableau est minime
user1032657
Vous n'avez apparemment pas mesuré le schéma de boucle inverse.
Hot Licks
Le premier test est équivalent au schéma de boucle inverse.
user1032657
que se passe-t-il lorsque vous vous retrouvez avec votre nouveau tableau et que votre tableau précédent est nul? Vous devez copier le nouveau tableau dans quel nom le tableau précédent a été nommé (ou le renommer), sinon comment votre autre code le référencerait-il?
aremvee
17

Soit utiliser le compte à rebours en boucle sur les indices:

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 for (id object in array)boucle ou NSEnumerator.

Jens Ayton
la source
3
vous devez écrire votre réponse dans le code m8. haha presque raté.
FlowUI. SimpleUITesting.com
Ça n'est pas correct. "for (id object in array)" est plus rapide que "for (NSInteger i = array.count - 1; i> = 0; --i)", et est appelé itération rapide. L'utilisation de l'itérateur est nettement plus rapide que l'indexation.
jack
@jack - Mais si vous supprimez un élément sous un itérateur, vous créez généralement des ravages. (Et une "itération rapide" n'est pas toujours beaucoup plus rapide.)
Hot Licks
La réponse est de 2008. L'itération rapide n'existait pas.
Jens Ayton
12

Pour iOS 4+ ou OS X 10.6+, Apple a ajouté une passingTestsérie 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];
zavié
la source
12

De nos jours, vous pouvez utiliser une énumération inversée basée sur des blocs. 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"]];
vikingosegundo
la source
Y a-t-il une garantie que la baie ne serait pas réallouée?
Cfr
Que voulez-vous dire par réallocation?
vikingosegundo
Tout en supprimant l'élément de la structure linéaire, il peut être efficace d'allouer un nouveau bloc de mémoire contigu et d'y déplacer tous les éléments. Dans ce cas, tous les pointeurs seraient invalidés.
Cfr
Comme l'opération de suppression ne saurait pas combien d'éléments seront pariés supprimés à la fin, cela n'aurait aucun sens de le faire lors de la suppression. Quoi qu'il en soit: détail d'implémentation, nous devons faire confiance à apple pour écrire du code raisonnable.
vikingosegundo
Le premier n'est pas plus agréable (parce que vous devez penser beaucoup à la façon dont cela fonctionne en premier lieu) et le second n'est pas une refactorisation sûre. Donc à la fin, je me suis retrouvé avec une solution classique acceptée qui est assez lisible en fait.
Renetik
8

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

[theArray filterUsingPredicate:aPredicate]

@Nathan devrait être très efficace

elp
la source
6

Voici la manière simple 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 effectuez l'énumération à travers une copie du tableau à supprimer, les deux contenant les mêmes objets. Un NSArray ne contient que des pointeurs d'objet, ce qui est donc très bon en termes de mémoire / performances.

Matjan
la source
2
Ou encore plus simple -for (LineItem *item in self.lineItems.copy)
Alexandre G
5

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

Nathan Kinsinger
la source
5

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...

Pokot0
la source
Bien que peu orthodoxe, j'ai trouvé que l'itération en arrière et la suppression étaient une solution propre et simple. Habituellement, c'est aussi l'une des méthodes les plus rapides.
rpetrich
Quel est le problème avec ça? Il est propre, rapide, facilement lisible et fonctionne comme un charme. Pour moi, cela ressemble à la meilleure réponse. Pourquoi at-il un score négatif? Est-ce que j'ai râté quelque chose?
Steph Thirion
1
@Steph: La question indique "quelque chose de plus élégant que la mise à jour manuelle de l'index".
Steve Madsen
1
Oh. J'avais manqué la partie je-ne-veux-absolument-pas-mettre-à-jour-l'index-manuellement. merci steve. OMI, cette solution est plus élégante que celle choisie (aucun tableau temporaire nécessaire), donc les votes négatifs contre elle semblent injustes.
Steph Thirion
@Steve: Si vous vérifiez les modifications, cette partie a été ajoutée après que j'ai soumis ma réponse .... sinon, j'aurais répondu "Itérer en arrière EST la solution la plus élégante" :). Bonne journée!
Pokot0
1

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

Paul Croarkin
la source
1

Que diriez-vous de permuter les éléments que vous souhaitez supprimer avec le 'n'th élément,' n-1'th élément et ainsi de suite?

Lorsque vous avez terminé, vous redimensionnez le tableau à la `` taille précédente - nombre de swaps ''

Cyber ​​Oliveira
la source
1

Si tous les objets de votre tableau sont uniques ou si vous souhaitez supprimer toutes les occurrences d'un objet lorsqu'ils sont trouvés, vous pouvez énumérer rapidement une copie du 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];
    }
}
lajos
la source
que se passe-t-il si l'original myArray est mis à jour pendant +arrayWithArrayson exécution?
bioffe
1
@bioffe: Ensuite, vous avez un bug dans votre code. NSMutableArray n'est pas thread-safe, vous devez contrôler l'accès via des verrous. Voir cette réponse
dreamlax
1

La réponse de benzado ci-dessus est ce que vous devez faire pour la préforme. Dans l'une de mes applications, removeObjectsInArray a pris 1 minute, l'ajout à un nouveau tableau a pris 0,023 seconde.

kaiser
la source
1

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];
}];
Kristopher Johnson
la source
1

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 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;
}];
Werner Altewischer
la source
0

Itérer en arrière était mon préféré pendant des années, mais pendant longtemps je n'ai jamais rencontré le cas où l'objet «le plus profond» (nombre le plus élevé) avait été retiré en premier. Momentanément avant que le pointeur ne passe à l'index suivant, il n'y a rien et il se bloque.

La méthode 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];
aremvee
la source