La mejor manera de eliminar de NSMutableArray mientras itera?


En Cocoa, si quiero hacer un bucle a través de un NSMutableArray y eliminar varios objetos que se ajustan a un cierto criterio, ¿cuál es la mejor manera de hacer esto sin reiniciar el bucle cada vez que elimino un objeto?

Gracias,

Editar: Solo para aclarar - Estaba buscando la mejor manera, por ejemplo, algo más elegante que actualizar manualmente el índice en el que estoy. Por ejemplo, en C++ puedo hacer;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
Author: Ned Batchelder, 2008-09-21

20 answers

Para mayor claridad, me gusta hacer un bucle inicial donde recojo los elementos para eliminar. Entonces los borro. Aquí hay un ejemplo usando la sintaxis de Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Entonces no hay duda acerca de si los índices se están actualizando correctamente, u otros pequeños detalles de contabilidad.

Editado para añadir:

Se ha señalado en otras respuestas que la formulación inversa debería ser más rápida. es decir, si itera a través de la matriz y compone una nueva matriz de objetos para mantener, en lugar de objetos a descartar. Eso puede ser cierto (aunque ¿qué pasa con la memoria y el costo de procesamiento de asignar una nueva matriz, y descartar la antigua?) pero incluso si es más rápido puede no ser tan importante como lo sería para una implementación ingenua, porque NSArrays no se comportan como arrays "normales". Hablan la charla pero caminan un camino diferente. Ver un buen análisis aquí:

La formulación inversa puede ser más rápida, pero nunca he tenido que preocuparme si lo es, porque lo anterior formulación siempre ha sido lo suficientemente rápido para mis necesidades.

Para mí, el mensaje para llevar a casa es usar cualquier formulación que sea más clara para ti. Optimizar solo si es necesario. Personalmente encuentro la formulación anterior más clara, por lo que lo uso. Pero si la formulación inversa es más clara para ti, ve por ella.

 375
Author: Christopher Ashworth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-21 02:33:51

Una variación más. Así se obtiene legibilidad y buen rendimiento:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
 79
Author: Corey Floyd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-06-19 20:33:29

Este es un problema muy simple. Solo tienes que iterar hacia atrás:

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

Este es un patrón muy común.

 39
Author: Hot Licks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-21 20:55:19

Algunas de las otras respuestas tendrían un rendimiento pobre en arreglos muy grandes, porque métodos como removeObject: y removeObjectsInArray: implican hacer una búsqueda lineal del receptor, que es un desperdicio porque ya sabes dónde está el objeto. Además, cualquier llamada a removeObjectAtIndex: tendrá que copiar valores desde el índice hasta el final de la matriz por una ranura a la vez.

Más eficiente sería lo siguiente:

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

Debido a que establecemos la capacidad de itemsToKeep, no perdemos tiempo copiando valores durante un cambio de tamaño. No modificamos la matriz en su lugar, por lo que somos libres de usar la Enumeración rápida. Usar setArray: para reemplazar el contenido de array con itemsToKeep será eficiente. Dependiendo de su código, incluso podría reemplazar la última línea con:

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

Así que ni siquiera es necesario copiar valores, solo intercambiar un puntero.

 38
Author: benzado,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-24 08:51:49

Puede usar NSPredicate para eliminar elementos de su matriz mutable. Esto no requiere bucles for.

Por ejemplo, si tiene una NSMutableArray de nombres, puede crear un predicado como este:

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

La siguiente línea le dejará con una matriz que contiene solo nombres que comienzan con b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Si tiene problemas para crear los predicados que necesita, use este enlace para desarrolladores de apple.

 28
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-24 20:59:56

O bien usar la cuenta regresiva de bucle sobre índices:

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

O hacer una copia con los objetos que desea conservar.

En particular, no utilice un bucle for (id object in array) o NSEnumerator.

 17
Author: Jens Ayton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-21 20:22:28

Hice una prueba de rendimiento utilizando 4 métodos diferentes. Cada prueba iteró a través de todos los elementos en una matriz de 100,000 elementos y eliminó cada 5to elemento. Los resultados no variaron mucho con/ sin optimización. Estos se hicieron en un iPad 4:

(1) removeObjectAtIndex: -- 271 ms

(2) removeObjectsAtIndexes: -- 1010 ms (porque construir el conjunto de índices toma ~700 ms; de lo contrario, esto es básicamente lo mismo que llamar a removeObjectAtIndex: para cada elemento)

(3) removeObjects: -- 326 ms

(4) hacer una nueva matriz con objetos que pasan la prueba {17 ms

Por lo tanto, crear una nueva matriz es, con mucho, la más rápida. Los otros métodos son todos comparables, excepto que el uso de removeObjectsAtIndexes: será peor con más elementos para eliminar, debido al tiempo necesario para construir el conjunto de índices.

 16
Author: user1032657,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-06-03 21:04:53

Para iOS 4+ o OS X 10.6+, Apple agregó passingTest una serie de API en NSMutableArray, como – indexesOfObjectsPassingTest:. Una solución con dicha API sería:

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];
 12
Author: zavié,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-18 16:41:11

Hoy en día se puede utilizar la enumeración basada en bloques invertidos. Un código de ejemplo 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];
}];

Resultado:

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

Otra opción con una sola línea de código:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];
 12
Author: vikingosegundo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-07 23:39:29

De una manera más declarativa, dependiendo de los criterios que coincidan con los elementos a eliminar, podría usar:

[theArray filterUsingPredicate:aPredicate]

@Nathan debería ser muy eficiente

 8
Author: elp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-28 14:03:27

Aquí está la manera fácil y limpia. Me gusta duplicar mi matriz justo en la llamada de enumeración rápida:

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

De esta manera se enumera a través de una copia de la matriz de la que se elimina, ambos sosteniendo los mismos objetos. Un NSArray contiene punteros de objeto solo por lo que esto es totalmente fino memoria/rendimiento sabio.

 6
Author: Matjan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-21 02:26:58

Agregue los objetos que desea eliminar a una segunda matriz y, después del bucle, use-removeObjectsInArray:.

 5
Author: Nathan Kinsinger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-21 19:50:41

Esto debería hacerlo:

    NSMutableArray* myArray = ....;

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

Espero que esto ayude...

 5
Author: Pokot0,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-21 19:55:11

¿Por qué no agrega los objetos que se eliminarán a otro NSMutableArray? Cuando haya terminado de iterar, puede eliminar los objetos que ha recopilado.

 1
Author: Paul Croarkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-21 19:51:40

¿Qué tal intercambiar los elementos que desea eliminar con el elemento 'n', el elemento 'n-1' y así sucesivamente?

Cuando haya terminado, cambie el tamaño de la matriz a 'tamaño anterior - número de swaps'

 1
Author: Leonardo Constantino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-21 21:12:19

Si todos los objetos de su matriz son únicos o desea eliminar todas las ocurrencias de un objeto cuando se encuentre, puede enumerar rápidamente en una copia de matriz y usar [NSMutableArray removeObject:] para eliminar el objeto del original.

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

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}
 1
Author: lajos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-22 05:06:19

El anwser de Benzado anterior es lo que debe hacer para preformace. En una de mis aplicaciones removeObjectsInArray tomó un tiempo de ejecución de 1 minuto, simplemente añadiendo a una nueva matriz tomó .023 segundos.

 1
Author: Kaiser,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-17 21:31:00

Defino una categoría que me permite filtrar usando un bloque, así:

@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

Que luego se puede usar así:

[myMutableArray filterUsingTest:^BOOL(id obj, NSUInteger idx) {
    return [self doIWantToKeepThisObject:obj atIndex:idx];
}];
 1
Author: Kristopher Johnson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-23 13:10:36

Una mejor implementación podría ser usar el método category en 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

El bloque de predicados se puede implementar para hacer el procesamiento en cada objeto en la matriz. Si el predicado devuelve true, el objeto se elimina.

Un ejemplo para una matriz de fechas para eliminar todas las fechas que se encuentran en el pasado:

NSMutableArray *dates = ...;
[dates removeObjectsWithPredicate:^BOOL(id obj) {
    NSDate *date = (NSDate *)obj;
    return [date timeIntervalSinceNow] < 0;
}];
 1
Author: Werner Altewischer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-27 09:58:43

Iterar hacia atrás-ly fue mi favorito durante años , pero durante mucho tiempo nunca encontré el caso en el que el objeto 'más profundo' ( recuento más alto) se eliminó primero. Momentáneamente antes de que el puntero se mueva al siguiente índice no hay nada y se bloquea.

La manera de Benzado es la más cercana a lo que hago ahora, pero nunca me di cuenta de que habría una reorganización de la pila después de cada eliminación.

Bajo Xcode 6 esto funciona

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];
 0
Author: aremvee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-10 07:27:48