Лучший способ удалить из NSMutableArray во время итерации?

В Cocoa, если я хочу пропустить NSMutableArray и удалить несколько объектов, которые соответствуют определенным критериям, что лучший способ сделать это без перезапуска цикла каждый раз, когда я удаляю объект?

Спасибо,

Изменить: просто уточнить - я искал лучший способ, например. что-то более элегантное, чем ручное обновление индекса, на котором я нахожусь. Например, в С++ я могу сделать;

iterator it = someList.begin();

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

Ответ 1

Для ясности я хотел бы создать начальный цикл, где я собираю элементы для удаления. Затем я удаляю их. Здесь образец с использованием синтаксиса Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Тогда нет никаких сомнений в том, правильно ли обновляются индексы или какие-то небольшие подробности бухгалтерского учета.

Отредактировано для добавления:

В других ответах было отмечено, что обратная формулировка должна быть более быстрой. т.е. если вы выполняете итерацию по массиву и создаете новый массив объектов, чтобы сохранить, а не объекты для удаления. Это может быть правдой (хотя, что связано с объемом памяти и стоимостью обработки при распределении нового массива и отбрасыванием старого?), Но даже если это будет быстрее, это может быть не так дорого, как это было бы для наивной реализации, потому что NSArrays не ведут себя как "обычные" массивы. Они разговаривают, но ходят по-другому. Посмотрите хороший анализ здесь:

Обратная формулировка может быть более быстрой, но мне никогда не нужно было заботиться о том, действительно ли это, потому что эта формулировка всегда была достаточно быстрой для моих нужд.

Для меня сообщение о возврате - это использовать любую формулировку для вас. Оптимизируйте только при необходимости. Я лично считаю эту формулировку максимально ясной, поэтому я ее и использую. Но если обратная формулировка яснее для вас, идите за ней.

Ответ 2

Еще одно изменение. Таким образом, вы получаете читаемость и хорошую производительность:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

Ответ 3

Некоторые из других ответов будут иметь низкую производительность на очень больших массивах, потому что методы, такие как removeObject: и removeObjectsInArray:, включают в себя выполнение линейного поиска приемника, что является отходом, потому что вы уже знаете, где находится объект. Кроме того, любой вызов removeObjectAtIndex: должен будет скопировать значения из индекса в конец массива на один слот за раз.

Более эффективным будет следующее:

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

Поскольку мы устанавливаем емкость itemsToKeep, мы не тратим время на копирование значений во время изменения размера. Мы не модифицируем массив на месте, поэтому мы можем использовать Fast Enumeration. Использование setArray: для замены содержимого array на itemsToKeep будет эффективным. В зависимости от вашего кода вы могли бы даже заменить последнюю строку:

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

Поэтому нет необходимости копировать значения, а только менять указатель.

Ответ 4

За крик! Это очень простая проблема. Вы просто повторяете назад:

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

Это очень распространенная картина, но, похоже, только Йенс ее получил.

Ответ 5

Вы можете использовать NSpredicate для удаления элементов из вашего изменяемого массива. Для циклов это не требуется.

Например, если у вас есть NSMutableArray имен, вы можете создать предикат, подобный этому:

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

Следующая строка оставит вас с массивом, который содержит только имена, начинающиеся с b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Если вам не удается создать предикаты, которые вам нужны, используйте ссылку apple developer.

Ответ 6

Либо использовать цикл, подсчитывающий вниз по индексам - для (NSInteger я = array.count - 1; я >= 0; --i) - или сделать копию с объектами, которые вы хотите сохранить. В частности, не используйте цикл for (id object in array) или NSEnumerator.

Ответ 7

Я провел тест производительности, используя 4 разных метода. Каждый тест повторялся через все элементы в массиве из 100 000 элементов и удалял каждый пятый элемент. Результаты не сильно отличались с оптимизацией/без нее. Они были сделаны на iPad 4:

(1) removeObjectAtIndex: - 271 мс

(2) removeObjectsAtIndexes: - 1010 мс (поскольку создание набора индексов занимает ~ 700 мс, в противном случае это в основном то же самое, что и вызов removeObjectAtIndex: для каждого элемента)

(3) removeObjects: - 326 мс

(4) создать новый массив с объектами, проходящими тест - 17 мс

Итак, создание нового массива является самым быстрым. Другие методы сопоставимы, за исключением того, что использование removeObjectsAtIndexes: будет хуже с большим количеством элементов для удаления из-за времени, необходимого для создания набора индексов.

Ответ 8

Для iOS 4+ или OS X 10.6+ Apple добавила passingTest серии API в NSMutableArray, например – indexesOfObjectsPassingTest:. Решение с таким API было бы:

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

Ответ 9

В настоящее время вы можете использовать перепоставку на основе блоков. Простой примерный код:

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

Результат:

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

другой вариант с одной строкой кода:

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

Ответ 10

В более декларативном виде, в зависимости от критериев, соответствующих элементам для удаления, вы можете использовать:

[theArray filterUsingPredicate:aPredicate]

@Натан должен быть очень эффективным

Ответ 11

Здесь легкий и чистый способ. Мне нравится дублировать мой массив прямо в вызове быстрого перечисления:

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

Таким образом вы перечисляете экземпляр удаляемого массива, оба сохраняя одни и те же объекты. NSArray содержит объектные указатели, так что это абсолютно точная память/производительность.

Ответ 12

Добавьте объекты, которые вы хотите удалить, во второй массив, а после цикла используйте -removeObjectsInArray:.

Ответ 13

это должно сделать это:

    NSMutableArray* myArray = ....;

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

надеюсь, что это поможет...

Ответ 14

Почему бы вам не добавить объекты для удаления в другой NSMutableArray. Когда вы закончите итерацию, вы можете удалить объекты, которые вы собрали.

Ответ 15

Как насчет замены элементов, которые вы хотите удалить, с помощью "n-го элемента", n-1-го элемента и т.д.?

Когда вы закончите, вы измените размер массива на "предыдущий размер - количество свопов"

Ответ 16

Если все объекты в вашем массиве уникальны или вы хотите удалить все вхождения объекта при его обнаружении, вы можете быстро перечислить экземпляр массива и использовать [NSMutableArray removeObject:], чтобы удалить объект из оригинала.

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

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

Ответ 17

benzado anwser выше - это то, что вы должны сделать для preformace. В одном из моих приложений removeObjectsInArray занял время работы 1 минута, просто добавив к новому массиву заняло 0,023 секунды.

Ответ 18

Я определяю категорию, которая позволяет мне фильтровать с помощью блока, например:

@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

который затем можно использовать следующим образом:

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

Ответ 19

Более приятной реализацией может быть использование метода категории ниже для 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

Блок предикатов может быть реализован для обработки на каждом объекте в массиве. Если предикат возвращает true, объект удаляется.

Пример для массива дат для удаления всех дат, которые лежат в прошлом:

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

Ответ 20

Итерация назад - моя любимая годами, но долгое время я никогда не сталкивался с тем случаем, когда сначала был удален объект "самый глубокий" (самый высокий счет). На мгновение, прежде чем курсор перейдет к следующему индексу, ничего не произойдет, и он сработает.

Benzado way ближе всего к тому, что я делаю сейчас, но я никогда не понимал, что после каждого удаления будут перестановки стека.

в Xcode 6 это работает

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