Il modo migliore per rimuovere da NSMutableArray durante l'iterazione?


194

In Cocoa, se voglio eseguire un ciclo attraverso un NSMutableArray e rimuovere più oggetti che soddisfano determinati criteri, qual è il modo migliore per farlo senza riavviare il ciclo ogni volta che rimuovo un oggetto?

Grazie,

Modifica: solo per chiarire: stavo cercando il modo migliore, ad esempio qualcosa di più elegante dell'aggiornamento manuale dell'indice in cui mi trovo. Ad esempio in C ++ posso fare;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}

9
Passa da dietro in avanti.
Hot Licks,

Nessuno risponde al "PERCHÉ"
onmyway133

1
@HotLicks Uno dei miei preferiti di tutti i tempi e la soluzione più sottovalutata nella programmazione in generale: D
Julian F. Weinert

Risposte:


388

Per chiarezza, mi piace creare un ciclo iniziale in cui raccolgo gli elementi da eliminare. Quindi li elimino. Ecco un esempio usando la sintassi di Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Quindi non ci sono dubbi sul fatto che gli indici vengano aggiornati correttamente o altri piccoli dettagli di contabilità.

Modificato per aggiungere:

È stato notato in altre risposte che la formulazione inversa dovrebbe essere più veloce. vale a dire se si esegue l'iterazione attraverso l'array e si compone un nuovo array di oggetti da conservare, anziché gli oggetti da scartare. Questo può essere vero (anche se per quanto riguarda la memoria e il costo di elaborazione dell'allocazione di un nuovo array e dell'eliminazione di quello vecchio?) Ma anche se è più veloce, potrebbe non essere un grosso problema come sarebbe per un'implementazione ingenua, perché NSArrays non si comportano come matrici "normali". Parlano, ma camminano per una passeggiata diversa. Vedi una buona analisi qui:

La formulazione inversa potrebbe essere più veloce, ma non ho mai avuto bisogno di preoccuparmi, perché la formulazione sopra è sempre stata abbastanza veloce per le mie esigenze.

Per me il messaggio da portare a casa è di usare qualunque sia la formulazione più chiara per te. Ottimizza solo se necessario. Personalmente trovo la formulazione di cui sopra più chiara, motivo per cui la utilizzo. Ma se la formulazione inversa ti è più chiara, provaci.


47
Attenzione che ciò potrebbe creare bug se gli oggetti si trovano più di una volta in un array. In alternativa, è possibile utilizzare un NSMutableIndexSet e - (void) removeObjectsAtIndexes.
Georg Schölly,

1
In realtà è più veloce fare l'inverso. La differenza di prestazioni è piuttosto grande, ma se il tuo array non è così grande, i tempi saranno comunque insignificanti.
user1032657,

Ho usato revers ... ho creato l'array come zero ... poi ho aggiunto solo quello che volevo e ricaricato i dati ... fatto ... ho creato dummyArray che è il mirror dell'array principale in modo da avere i dati effettivi principali
Fahim Parkar

La memoria aggiuntiva deve essere allocata per fare inversa. Non è un algoritmo sul posto e in alcuni casi non è una buona idea. Quando rimuovi direttamente sposta semplicemente l'array (che è molto efficiente poiché non si sposta uno per uno, può spostare un grosso pezzo), ma quando crei un nuovo array hai bisogno di tutto l'allocazione e l'assegnazione. Quindi, se elimini solo alcuni elementi, l'inverso sarà molto più lento. È un tipico algoritmo sembra giusto.
jack

82

Un'altra variazione. Quindi ottieni leggibilità e buone prestazioni:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

Questo e spettacolare. Stavo cercando di rimuovere più elementi dall'array e questo ha funzionato perfettamente. Altri metodi stavano causando problemi :) Grazie amico
AbhinavVinay

Corey, questa risposta (sotto) ha detto che removeObjectsAtIndexesè il metodo peggiore per rimuovere gli oggetti, sei d'accordo? Lo sto chiedendo perché la tua risposta è troppo vecchia ora. Sarebbe comunque bello scegliere il migliore?
Hemang,

Mi piace molto questa soluzione in termini di leggibilità. Come è in termini di prestazioni rispetto alla risposta di @HotLicks?
Victor Maia Aldecôa,

Questo metodo dovrebbe essere decisamente incoraggiato durante l'eliminazione di oggetti dall'array in Obj-C.
Borea,

enumerateObjectsUsingBlock:ti procurerebbe l'incremento dell'indice gratuitamente.
pkamb,

42

Questo è un problema molto semplice Basta iterare all'indietro:

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

Questo è un modello molto comune.


avrebbe perso la risposta di Jens, perché non ha scritto il codice: P .. thnx m8
FlowUI. SimpleUITesting.com

3
Questo è il metodo migliore. È importante ricordare che la variabile di iterazione dovrebbe essere firmata, nonostante gli indici di array in Objective-C siano dichiarati come non firmati (posso immaginare come Apple se ne rammarica ora).
Mojuba,

39

Alcune delle altre risposte avrebbero scarse prestazioni su array molto grandi, perché i metodi gradiscono removeObject:e removeObjectsInArray:comportano una ricerca lineare del ricevitore, il che è uno spreco perché sai già dove si trova l'oggetto. Inoltre, qualsiasi chiamata aremoveObjectAtIndex: dovrà copiare i valori dall'indice alla fine dell'array di uno slot alla volta.

Più efficiente sarebbe il seguente:

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

Poiché impostiamo la capacità di itemsToKeep, non perdiamo tempo a copiare i valori durante un ridimensionamento. Non modifichiamo l'array sul posto, quindi siamo liberi di usare l'Enumerazione veloce. L'uso setArray:per sostituire il contenuto di arraycon itemsToKeepsarà efficiente. A seconda del codice, puoi persino sostituire l'ultima riga con:

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

Quindi non è nemmeno necessario copiare valori, scambiare solo un puntatore.


Questo algo si rivela buono in termini di complessità temporale. Sto cercando di capirlo in termini di complessità spaziale. Ho la stessa implementazione andando in giro dove ho impostato la capacità di 'itemsToKeep' con l'attuale 'Conteggio array'. Ma dire che non voglio pochi oggetti dall'array, quindi non lo aggiungo agli articoli ToKeep. Quindi quella capacità dei miei oggetti ToKeep è 10, ma in realtà conservo solo 6 oggetti. Questo significa che sto sprecando spazio di 4 oggetti? PS non sto trovando difetti, sto solo cercando di capire la complessità dell'algo. :)
tech_human

Sì, hai spazio riservato per quattro puntatori che non stai utilizzando. Tieni presente che l'array contiene solo puntatori, non gli oggetti stessi, quindi per quattro oggetti significa 16 byte (architettura a 32 bit) o ​​32 byte (64 bit).
benzado,

Si noti che qualsiasi soluzione sarà nella migliore delle ipotesi richiede 0 spazio aggiuntivo (si eliminano gli elementi sul posto) o nella peggiore delle ipotesi raddoppia la dimensione originale dell'array (perché si crea una copia dell'array). Ancora una volta, poiché abbiamo a che fare con puntatori e non con copie complete di oggetti, questo è abbastanza economico nella maggior parte dei casi.
benzado,

Ok capito. Grazie Benzado !!
tech_human,

Secondo questo post, il suggerimento di arrayWithCapacity: il metodo sulla dimensione dell'array non viene attualmente utilizzato.
Kremk,

28

È possibile utilizzare NSpredicate per rimuovere elementi dall'array modificabile. Questo richiede no per i loop.

Ad esempio, se si dispone di un NSMutableArray di nomi, è possibile creare un predicato come questo:

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

La seguente riga ti lascerà con un array che contiene solo nomi che iniziano con b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

In caso di problemi con la creazione dei predicati necessari, utilizzare questo collegamento per sviluppatori Apple .


3
Non richiede visibile per i loop. C'è un ciclo nell'operazione di filtro, semplicemente non lo vedi.
Hot Licks

18

Ho fatto un test delle prestazioni usando 4 metodi diversi. Ogni test è stato ripetuto attraverso tutti gli elementi in un array di 100.000 elementi e rimosso ogni 5 elementi. I risultati non sono variati molto con / senza ottimizzazione. Questi sono stati fatti su un iPad 4:

(1) removeObjectAtIndex:- 271 ms

(2) removeObjectsAtIndexes:- 1010 ms (perché la creazione dell'indice richiede ~ 700 ms; in caso contrario, ciò equivale sostanzialmente alla chiamata di removeObjectAtIndex: per ogni elemento)

(3) removeObjects:- 326 ms

(4) crea un nuovo array con oggetti che superano il test - 17 ms

Quindi, creare un nuovo array è di gran lunga il più veloce. Gli altri metodi sono tutti comparabili, ad eccezione del fatto che l'utilizzo di removeObjectsAtIndexes: peggiorerà con più elementi da rimuovere, a causa del tempo necessario per creare il set di indici.


1
Hai contato il tempo necessario per creare un nuovo array e successivamente trasferirlo? Non credo che possa farlo da solo in 17 ms. Più 75.000 incarichi?
jack

Il tempo necessario per creare e deallocare un nuovo array è minimo
user1032657,

Apparentemente non hai misurato lo schema ad anello inverso.
Hot Licks

Il primo test equivale allo schema ad anello inverso.
user1032657

cosa succede quando ti rimane il tuo nuovo Array e il tuo prevArray è Nulled? Dovresti copiare il nuovoArray con il nome (o rinominarlo) di PrevArray, altrimenti come farebbe riferimento il tuo altro codice?
Aremvee,

17

Utilizzare il conto alla rovescia del loop sugli indici:

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

o fai una copia con gli oggetti che vuoi conservare.

In particolare, non utilizzare un for (id object in array)loop o NSEnumerator.


3
dovresti scrivere la tua risposta nel codice m8. haha quasi perso.
FlowUI. SimpleUITesting.com

Questo non è giusto. "for (id id in array)" è più veloce di "for (NSInteger i = array.count - 1; i> = 0; --i)" e si chiama iterazione veloce. L'uso dell'iteratore è decisamente più veloce dell'indicizzazione.
jack

@jack - Ma se rimuovi un elemento da sotto un iteratore di solito crei il caos. (E "iterazione veloce" non è sempre molto più veloce.)
Hot Licks

La risposta è del 2008. L'iterazione rapida non esisteva.
Jens Ayton,

12

Per iOS 4+ o OS X 10.6+, Apple ha aggiunto una passingTestserie di API NSMutableArray, come – indexesOfObjectsPassingTest:. Una soluzione con tale API sarebbe:

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

12

Al giorno d'oggi è possibile utilizzare l'enumerazione inversa basata su blocchi. Un semplice codice di esempio:

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

Risultato:

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

un'altra opzione con una sola riga di codice:

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

C'è qualche garanzia che l'array non venga riallocato?
Cfr

Cosa intendi con riallocazione?
vikingosegundo,

Durante la rimozione dell'elemento dalla struttura lineare, potrebbe essere efficace allocare un nuovo blocco di memoria contiguo e spostare tutti gli elementi lì. In questo caso tutti i puntatori verrebbero invalidati.
Cfr

Poiché l'operazione di eliminazione non è in grado di sapere quanti elementi scommettono eliminati alla fine, non avrebbe senso farlo durante la rimozione. Comunque: dettagli di implementazione, dobbiamo fidarci di apple per scrivere un codice ragionevole.
vikingosegundo,

In primo luogo non è più bello (perché devi pensare molto a come funziona in primo luogo) e in secondo luogo non è il refactoring sicuro. Quindi alla fine ho concluso con una soluzione classica accettata che in realtà è abbastanza leggibile.
Renetik,

8

In un modo più dichiarativo, a seconda dei criteri corrispondenti agli elementi da rimuovere è possibile utilizzare:

[theArray filterUsingPredicate:aPredicate]

@Nathan dovrebbe essere molto efficiente


6

Ecco il modo semplice e pulito. Mi piace duplicare il mio array proprio nella chiamata di enumerazione veloce:

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

In questo modo si enumera attraverso una copia dell'array da cui si elimina, entrambi con gli stessi oggetti. Un NSArray contiene solo puntatori a oggetti, quindi questa è una memoria / prestazione totalmente fine.


2
O ancora più semplice -for (LineItem *item in self.lineItems.copy)
Alexandre G,

5

Aggiungi gli oggetti che desideri rimuovere a un secondo array e, dopo il ciclo, usa -removeObjectsInArray :.


5

questo dovrebbe farlo:

    NSMutableArray* myArray = ....;

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

spero che questo ti aiuti...


Anche se non ortodosso, ho trovato l'iterazione all'indietro e l'eliminazione mentre vado a essere una soluzione pulita e semplice. Di solito è anche uno dei metodi più veloci.
rpetrich,

Cosa c'è di sbagliato in questo? È pulito, veloce, facilmente leggibile e funziona come un fascino. A me sembra la risposta migliore. Perché ha un punteggio negativo? Mi sto perdendo qualcosa qui?
Steph Thirion,

1
@Steph: la domanda afferma "qualcosa di più elegante dell'aggiornamento manuale dell'indice".
Steve Madsen,

1
Oh. Avevo perso la parte I-assolutamente-non-voglio-aggiornare-l'indice-manualmente. grazie steve. IMO questa soluzione è più elegante di quella scelta (non è necessario un array temporaneo), quindi i voti negativi contro di essa sembrano ingiusti.
Steph Thirion,

@Steve: se controlli le modifiche, quella parte è stata aggiunta dopo che ho inviato la mia risposta .... In caso contrario, avrei risposto con "Iterare all'indietro È la soluzione più elegante" :). Buona giornata!
Pokot0

1

Perché non aggiungere gli oggetti da rimuovere a un altro NSMutableArray. Al termine dell'iterazione, è possibile rimuovere gli oggetti raccolti.


1

Che ne dici di scambiare gli elementi che vuoi eliminare con 'n'th element,' n-1'th element e così via?

Al termine ridimensionare l'array in "dimensione precedente - numero di swap"


1

Se tutti gli oggetti nell'array sono univoci o si desidera rimuovere tutte le occorrenze di un oggetto una volta trovato, è possibile enumerare rapidamente una copia dell'array e utilizzare [NSMutableArray removeObject:] per rimuovere l'oggetto dall'originale.

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

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

cosa succede se myArray originale viene aggiornato mentre +arrayWithArrayviene eseguito?
Bioffe

1
@bioffe: allora hai un bug nel tuo codice. NSMutableArray non è thread-safe, ci si aspetta che controlli l'accesso tramite blocchi. Vedi questa risposta
dreamlax,

1

La risposta di benzado sopra è ciò che dovresti fare per il preformace. In una delle mie applicazioni, removeObjectsInArray ha richiesto un tempo di esecuzione di 1 minuto, mentre l'aggiunta a un nuovo array ha richiesto 0,023 secondi.


1

Definisco una categoria che mi permette di filtrare usando un blocco, come questo:

@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

che può quindi essere utilizzato in questo modo:

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

1

Un'implementazione migliore potrebbe essere l'utilizzo del metodo di categoria riportato di seguito su 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

Il blocco predicato può essere implementato per eseguire l'elaborazione su ciascun oggetto dell'array. Se il predicato restituisce true, l'oggetto viene rimosso.

Un esempio per un array di date per rimuovere tutte le date che si trovano in passato:

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

0

Iterare all'indietro era il mio preferito per anni, ma per molto tempo non ho mai incontrato il caso in cui l'oggetto "più profondo" (conteggio più alto) è stato rimosso per primo. Momentaneamente prima che il puntatore passi all'indice successivo non c'è nulla e si blocca.

Il modo in cui Benzado è il più vicino a quello che faccio ora, ma non ho mai capito che ci sarebbe stato il rimpasto dello stack dopo ogni rimozione.

sotto Xcode 6 funziona

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];
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.