Лучший способ удалить из NSMutableArray во время итерации? - PullRequest
194 голосов
/ 21 сентября 2008

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

Спасибо

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

iterator it = someList.begin();

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

Ответы [ 20 ]

383 голосов
/ 22 сентября 2008

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

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

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

Отредактировано, чтобы добавить:

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

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

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

82 голосов
/ 20 июня 2009

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

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
41 голосов
/ 27 августа 2013

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

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

Это очень распространенный шаблон.

39 голосов
/ 24 сентября 2008

Некоторые другие ответы будут иметь плохую производительность на очень больших массивах, потому что такие методы, как 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, мы не тратим время на копирование значений во время изменения размера. Мы не модифицируем массив на месте, поэтому мы можем использовать быстрое перечисление. Использование setArray: для замены содержимого array на itemsToKeep будет эффективным. В зависимости от вашего кода, вы можете даже заменить последнюю строку на:

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

Так что даже копировать значения не нужно, только поменяйте местами указатель.

28 голосов
/ 25 сентября 2008

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

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

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

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

[namesArray filterUsingPredicate:caseInsensitiveBNames];

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

18 голосов
/ 04 июня 2013

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

(1) removeObjectAtIndex: - 271 мс

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

(3) removeObjects: - 326 мс

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

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

17 голосов
/ 21 сентября 2008

Либо используйте цикл обратного отсчета по индексам:

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

или сделайте копию с объектами, которые вы хотите сохранить.

В частности, не используйте петлю for (id object in array) или NSEnumerator.

12 голосов
/ 28 августа 2013

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

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"]];
12 голосов
/ 18 августа 2012

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

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];
8 голосов
/ 22 сентября 2008

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

[theArray filterUsingPredicate:aPredicate]

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...