Pregunta

En Cocoa, si quiero recorrer un NSMutableArray y eliminar varios objetos que se ajustan a 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, p.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);
}
¿Fue útil?

Solución

Para mayor claridad, me gusta hacer un bucle inicial donde recopilo los elementos que debo eliminar.Luego los borro.Aquí hay un ejemplo que utiliza 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 actualizan correctamente u otros pequeños detalles contables.

Editado para agregar:

En otras respuestas se ha observado 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 conservar, en lugar de objetos para descartar.Eso puede ser cierto (aunque ¿qué pasa con el costo de memoria y procesamiento de asignar una nueva matriz y descartar la anterior?), pero incluso si es más rápido, puede que no sea tan importante como lo sería para una implementación ingenua, porque NSArrays no se comporte como matrices "normales".Hablan lo que hablan pero caminan por un camino diferente. Vea un buen análisis aquí:

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

Para mí, la conclusión es utilizar la formulación que le resulte más clara.Optimice solo si es necesario.Personalmente, la formulación anterior me parece más clara, por eso la uso.Pero si te resulta más clara la formulación inversa, hazlo.

Otros consejos

Una variación 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];

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

Algunas de las otras respuestas tendrían un rendimiento deficiente en matrices muy grandes, porque métodos como removeObject: y removeObjectsInArray: Implica hacer una búsqueda lineal del receptor, lo cual es un desperdicio porque ya sabes dónde está el objeto.Además, cualquier llamada a removeObjectAtIndex: Tendrá que copiar los valores desde el índice hasta el final de la matriz, 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];

Porque 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 podemos usar la enumeración rápida.Usando setArray: para reemplazar el contenido de array con itemsToKeep será eficiente.Dependiendo de tu código, incluso podrías reemplazar la última línea con:

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

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

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

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 le dejará con una matriz que contiene sólo nombres que comienzan con b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Si tiene problemas para crear los predicados que necesita, utilice este enlace de desarrollador de Apple.

Hice una prueba de rendimiento utilizando 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 artículo)

(3) removeObjects: -- 326 ms

(4) crear una nueva matriz con objetos que pasen la prueba: 17 ms

Por lo tanto, crear una nueva matriz es, con diferencia, la forma más rápida.Los demás métodos son todos comparables, excepto el que utiliza removeObjectsAtIndexes:Será peor si hay más elementos que eliminar, debido al tiempo necesario para crear el conjunto de índices.

Utilice la cuenta atrás en bucle sobre los índices:

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

o haz una copia con los objetos que quieras conservar.

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

Para iOS 4+ u OS X 10.6+, Apple agregó passingTest 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];

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 solo una línea de código:

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

De forma más declarativa, dependiendo de los criterios que coincidan con los elementos a eliminar, podría utilizar:

[theArray filterUsingPredicate:aPredicate]

@Nathan debería ser muy eficiente

Aquí tienes la forma 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 una copia de la matriz que se está eliminando, y ambas contienen los mismos objetos.Un NSArray solo contiene punteros de objetos, por lo que esto es totalmente bueno en cuanto a memoria/rendimiento.

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

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

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

¿Qué tal intercambiar los elementos que desea eliminar con el elemento 'n-ésimo, el elemento 'n-1', etc.?

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

Si todos los objetos en su matriz son únicos o desea eliminar todas las apariciones de un objeto cuando lo encuentra, puede enumerar rápidamente en una copia de la 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];
    }
}

La respuesta anterior de Benzado es lo que debe hacer para obtener preformace.En una de mis aplicaciones, removeObjectsInArray tomó un tiempo de ejecución de 1 minuto, simplemente agregar a una nueva matriz tomó 0,023 segundos.

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 utilizar así:

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

Una implementación mejor podría ser utilizar el método de categoría siguiente 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 predicado se puede implementar para realizar procesamiento 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 que pertenecen al pasado:

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

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

El método de Benzado es el más cercano a lo que hago ahora, pero nunca me di cuenta de que se reorganizaría 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];
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top