¿La mejor manera de eliminar de NSMutableArray mientras itera?

194

En Cocoa, si quiero recorrer un NSMutableArray y eliminar varios objetos que cumplan un determinado criterio, ¿cuál es la mejor manera de hacerlo sin reiniciar el ciclo 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);
}
Andrew Grant
fuente
9
Bucle de atrás hacia adelante.
Hot Licks
Nadie responde el "POR QUÉ"
onmyway133
1
@HotLicks Uno de mis favoritos de todos los tiempos y la solución más subestimada en la programación en general: D
Julian F. Weinert

Respuestas:

388

Para mayor claridad, me gusta hacer un bucle inicial donde recopilo los elementos para eliminar. Luego los elimino. Aquí hay una muestra 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 dudas sobre si los índices se están actualizando correctamente u otros pequeños detalles de contabilidad.

Editado para agregar:

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

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

Para mí, el mensaje final es usar cualquier formulación que sea más clara para usted. Optimice solo si es necesario. Personalmente, considero que la formulación anterior es más clara, por eso la uso. Pero si la formulación inversa es más clara para usted, hágalo.

Christopher Ashworth
fuente
47
Tenga en cuenta que esto podría crear errores si los objetos están más de una vez en una matriz. Como alternativa, puede usar un NSMutableIndexSet y - (nulo) removeObjectsAtIndexes.
Georg Schölly
1
En realidad es más rápido hacer lo inverso. La diferencia de rendimiento es bastante grande, pero si su matriz no es tan grande, los tiempos serán triviales de cualquier manera.
user1032657
Usé el reverso ... hice la matriz como nula ... luego solo agregué lo que quería y volví a cargar los datos ... hecho ... creé dummyArray que es el espejo de la matriz principal para que tengamos los datos reales principales
Fahim Parkar
Se debe asignar memoria adicional para hacer inversa. No es un algoritmo in situ y no es una buena idea en algunos casos. Cuando elimina directamente, simplemente cambia la matriz (lo cual es muy eficiente ya que no se moverá uno por uno, puede cambiar una gran parte), pero cuando crea una nueva matriz necesita toda la asignación y asignación. Entonces, si solo elimina algunos elementos, el inverso será mucho más lento. Es un algoritmo típico que parece correcto.
Jack
82

Una variante más. Entonces obtienes 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];
Corey Floyd
fuente
Esto es asombroso Estaba tratando de eliminar varios elementos de la matriz y esto funcionó perfectamente. Otros métodos estaban causando problemas :) Gracias hombre
AbhinavVinay
Corey, esta respuesta (a continuación) dice que, removeObjectsAtIndexeses el peor método para eliminar los objetos, ¿estás de acuerdo con eso? Estoy preguntando esto porque tu respuesta es demasiado vieja ahora. ¿Aún así es bueno elegir el mejor?
Hemang
Me gusta mucho esta solución en términos de legibilidad. ¿Cómo es en términos de rendimiento en comparación con la respuesta @HotLicks?
Victor Maia Aldecôa
Este método definitivamente debe ser alentado al eliminar objetos de la matriz en Obj-C.
boreas
enumerateObjectsUsingBlock:te daría el incremento del índice gratis.
pkamb
42

Este es un problema muy simple. Simplemente iteras 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.

Hot Licks
fuente
Hubiera perdido la respuesta de Jens, porque no escribió el código: P .. thnx m8
FlowUI. SimpleUITesting.com
3
Este es el mejor método. Es importante recordar que la variable de iteración debe ser firmada, a pesar de que los índices de matriz en Objective-C se declaran como sin firmar (me imagino cómo Apple se arrepiente de eso ahora).
mojuba
39

Algunas de las otras respuestas tendrían malos resultados en las matrices muy grandes, ya que los métodos como removeObject:e removeObjectsInArray:implican hacer una búsqueda lineal del receptor, que es una pérdida debido a que ya sabe dónde está el objeto. Además, cualquier llamada aremoveObjectAtIndex: tendrá que copiar valores desde el índice hasta el final de la matriz en 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 el 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 arraycon itemsToKeepserá eficiente. Dependiendo de su código, incluso podría reemplazar la última línea con:

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

Por lo tanto, ni siquiera es necesario copiar valores, solo intercambiar un puntero.

benzado
fuente
Esto demuestra ser bueno en términos de complejidad de tiempo. Estoy tratando de entenderlo en términos de complejidad espacial. Tengo la misma implementación dando vueltas donde configuré la capacidad para 'itemsToKeep' con el 'recuento de matrices' real. Pero digamos que no quiero pocos objetos de la matriz, así que no lo agrego a itemsToKeep. Entonces, la capacidad de mis itemsToKeep es 10, pero en realidad solo almaceno 6 objetos. ¿Esto significa que estoy desperdiciando espacio de 4 objetos? PD: no encuentro fallas, solo trato de entender la complejidad de algo. :)
tech_human
Sí, tiene espacio reservado para cuatro punteros que no está utilizando. Tenga en cuenta que la matriz solo contiene punteros, no los objetos en sí, por lo que para cuatro objetos significa 16 bytes (arquitectura de 32 bits) o 32 bytes (64 bits).
benzado
Tenga en cuenta que cualquiera de las soluciones requerirá, en el mejor de los casos, 0 espacio adicional (elimina elementos en su lugar) o, en el peor de los casos, duplicará el tamaño original de la matriz (porque hace una copia de la matriz). Nuevamente, debido a que estamos tratando con punteros y no copias completas de objetos, esto es bastante barato en la mayoría de las circunstancias.
benzado
Ok, lo tengo. Gracias Benzado !!
tech_human
De acuerdo con esta publicación, la sugerencia de arrayWithCapacity: método sobre el tamaño de la matriz no se está utilizando realmente.
Kremk
28

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

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

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

La siguiente línea lo 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 de desarrollador de Apple .


fuente
3
No requiere visible para los bucles. Hay un bucle en la operación del filtro, simplemente no lo ves.
Hot Licks
18

Hice una prueba de rendimiento con 4 métodos diferentes. Cada prueba recorrió todos los elementos en una matriz de 100,000 elementos y eliminó cada quinto 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 lleva ~ 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, el más rápido. Los otros métodos son todos comparables, excepto que usar removeObjectsAtIndexes: será peor con más elementos para eliminar, debido al tiempo necesario para construir el conjunto de índices.

usuario1032657
fuente
1
¿Contaste el tiempo para hacer una nueva matriz y luego desasignarla? No creo que pueda hacer eso solo en 17 ms. ¿Más 75,000 asignaciones?
Jack
El tiempo necesario para crear y desasignar una nueva matriz es mínimo
usuario1032657
Aparentemente no midió el esquema de bucle inverso.
Hot Licks
La primera prueba es equivalente al esquema de bucle inverso.
user1032657
¿Qué sucede cuando te quedas con tu nueva matriz y tu matriz anterior se anula? Tendría que copiar la nueva matriz con el nombre de la matriz anterior (o cambiarle el nombre), de lo contrario, ¿cómo lo referenciaría su otro código?
aremvee
17

Utilice el ciclo de cuenta regresiva sobre los índices:

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

o haga una copia con los objetos que desea conservar.

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

Jens Ayton
fuente
3
deberías escribir tu respuesta en el código m8. jaja casi lo extraño.
FlowUI. SimpleUITesting.com
Esto no está bien. "for (id object in array)" es más rápido que "for (NSInteger i = array.count - 1; i> = 0; --i)", y se llama iteración rápida. Usar iterador es definitivamente más rápido que indexar.
Jack
@jack - Pero si elimina un elemento de debajo de un iterador, generalmente crea estragos. (Y la "iteración rápida" no siempre es mucho más rápida).
Hot Licks
La respuesta es de 2008. La iteración rápida no existía.
Jens Ayton
12

Para iOS 4+ u OS X 10.6+, Apple agregó passingTestseries 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];
zavié
fuente
12

Hoy en día puede usar la enumeración inversa basada en bloques. 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 solo una línea de código:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];
vikingosegundo
fuente
¿Hay alguna garantía de que la matriz no se reasigne?
Cfr
¿Qué quieres decir con reasignación?
vikingosegundo
Mientras se elimina el elemento de la estructura lineal, podría ser efectivo asignar un nuevo bloque de memoria contigua y mover todos los elementos allí. En este caso, todos los punteros quedarían invalidados.
Cfr
Como la operación de eliminación no sabría cuántos elementos apostarán eliminados al final, no tendría sentido hacerlo durante la eliminación. De todos modos: detalle de implementación, tenemos que confiar en Apple para escribir código razonable.
vikingosegundo
Primero, no es más agradable (porque tienes que pensar mucho en cómo funciona en primer lugar) y segundo no es una refactorización segura. Así que al final terminé con una solución clásica aceptada que es bastante legible en realidad.
Renetik
8

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

elp
fuente
6

Aquí está la manera fácil y limpia. Me gusta duplicar mi matriz directamente 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, enumera a través de una copia de la matriz que se está eliminando, ambos con los mismos objetos. Un NSArray contiene punteros de objetos solo, por lo que es una memoria / rendimiento totalmente buena.

Matjan
fuente
2
O incluso más simple -for (LineItem *item in self.lineItems.copy)
Alexandre G
5

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

Nathan Kinsinger
fuente
5

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

Pokot0
fuente
Aunque no es ortodoxo, he encontrado que iterar hacia atrás y eliminar a medida que avanzo es una solución limpia y simple. Por lo general, también es uno de los métodos más rápidos.
rpetrich
¿Qué tiene de malo esto? Es limpio, rápido, fácil de leer y funciona a las mil maravillas. Para mí parece la mejor respuesta. ¿Por qué tiene una puntuación negativa? ¿Me estoy perdiendo de algo?
Steph Thirion
1
@Steph: La pregunta dice "algo más elegante que actualizar manualmente el índice".
Steve Madsen
1
Oh. Me había perdido la parte I-absolutamente-no-quiero-actualizar-el-índice-manualmente. Gracias Steve. En mi opinión, esta solución es más elegante que la elegida (no se necesita una matriz temporal), por lo que los votos negativos en su contra se sienten injustos.
Steph Thirion
@Steve: Si marca las ediciones, esa parte se agregó después de haber enviado mi respuesta ... si no, hubiera respondido con "Iterar hacia atrás es la solución más elegante" :). ¡Que tengas un buen día!
Pokot0
1

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

Paul Croarkin
fuente
1

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

Cuando haya terminado, cambia el tamaño de la matriz a 'tamaño anterior: número de intercambios'

Cyber ​​Oliveira
fuente
1

Si todos los objetos en su matriz son únicos o si desea eliminar todas las apariciones de un objeto cuando se encuentra, 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];
    }
}
lajos
fuente
¿Qué sucede si myArray original se actualiza mientras +arrayWithArrayse ejecuta?
bioffe
1
@bioffe: Entonces tienes un error en tu código. NSMutableArray no es seguro para subprocesos, se espera que controle el acceso a través de bloqueos. Ver esta respuesta
dreamlax
1

La respuesta anterior de benzado es lo que debe hacer para el preformado. En una de mis aplicaciones, removeObjectsInArray tomó un tiempo de ejecución de 1 minuto, solo agregar a una nueva matriz tomó 0.023 segundos.

Emperador
fuente
1

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

@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];
}];
Kristopher Johnson
fuente
1

Una implementación más agradable podría ser usar el método de categoría a continuación 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 procesar en cada objeto de la matriz. Si el predicado devuelve verdadero, el objeto se elimina.

Un ejemplo de una matriz de fechas para eliminar todas las fechas anteriores:

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

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

El camino de Benzado es el más cercano 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];
aremvee
fuente