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

StackOverflow https://stackoverflow.com/questions/111866

  •  02-07-2019
  •  | 
  •  

Вопрос

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

Спасибо,

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

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
Это было полезно?

Решение

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

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

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

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

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

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

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

Другие советы

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

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

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

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

Это очень распространенная закономерность.

Некоторые из других ответов будут иметь низкую производительность на очень больших массивах, потому что такие методы, как 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];

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

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

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

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

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

[namesArray filterUsingPredicate:caseInsensitiveBNames];

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

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

(1) removeObjectAtIndex: -- 271 мс

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

(3) removeObjects: -- 326 мс

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

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

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

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

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

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

Для 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];

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

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

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

[theArray filterUsingPredicate:aPredicate]

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

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

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

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

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

это должно сработать:

    NSMutableArray* myArray = ....;

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

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

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

Как насчет замены элементов, которые вы хотите удалить, на 'n-й элемент', 'n-1-й элемент и так далее?

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

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

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

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

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

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

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

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

Повторение в обратном направлении было моим любимым в течение многих лет, но долгое время я никогда не сталкивался со случаем, когда "самый глубокий" объект (наибольшее количество объектов) удалялся первым.За мгновение до того, как указатель переходит к следующему индексу, там ничего нет, и он выходит из строя.

Способ Бензадо ближе всего к тому, что я делаю сейчас, но я никогда не предполагал, что после каждого удаления будет происходить перестановка стека.

в 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];
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top