Domanda

In Cocoa, se voglio scorrere 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 es. qualcosa di più elegante dell'aggiornamento manuale dell'indice in cui mi trovo. Ad esempio in C ++ posso farlo;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
È stato utile?

Soluzione

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 potrebbe essere vero (anche se per quanto riguarda la memoria e il costo di elaborazione dell'allocazione di un nuovo array e dello scarto di quello vecchio?) Ma anche se è più veloce potrebbe non essere un grosso problema come sarebbe per un'implementazione ingenua, perché NSArrays non comportarsi come " normale " array. Parlano, ma camminano per una passeggiata diversa. Guarda una buona analisi qui:

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

Per me il messaggio da portare a casa è usare qualunque formulazione ti sia più chiara. 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.

Altri suggerimenti

Un'altra variante. 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 è un problema molto semplice. Basta ripetere al contrario:

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

Questo è un modello molto comune.

Alcune delle altre risposte avrebbero scarse prestazioni su array molto grandi, perché metodi come removeObject: e removeObjectsInArray: implicano una ricerca lineare del ricevitore, che è uno spreco perché sai già dove si trova l'oggetto. Inoltre, qualsiasi chiamata a removeObjectAtIndex: 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 valori durante un ridimensionamento. Non modifichiamo l'array sul posto, quindi siamo liberi di usare l'Enumerazione veloce. L'uso di setArray: per sostituire il contenuto di array con itemsToKeep sarà efficace. 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.

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

Se hai problemi a creare i predicati di cui hai bisogno, usa questo sviluppatore di apple link .

Ho eseguito un test delle prestazioni utilizzando 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 del set di indici richiede ~ 700 ms; altrimenti questo è fondamentalmente lo stesso che chiamare removeObjectAtIndex: per ogni articolo )

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

Usa il conto alla rovescia del ciclo sugli indici:

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

o creane una copia con gli oggetti che vuoi conservare.

In particolare, non utilizzare un per (oggetto id in array) o NSEnumerator .

Per iOS 4+ o OS X 10.6+, Apple ha aggiunto la serie di API passingTest in NSMutableArray , come & # 8211; 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];

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

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

[theArray filterUsingPredicate:aPredicate]

@Nathan dovrebbe essere molto efficiente

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.

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

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

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

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"

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

La risposta di benzado sopra è cosa dovresti fare per la performance. 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.

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

Un'implementazione migliore potrebbe essere quella di utilizzare il metodo di categoria seguente 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 di 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;
}];

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];
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top