it-swarm.com.ru

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

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

Спасибо,

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

iterator it = someList.begin();

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

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

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

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

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

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

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

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

379
Christopher Ashworth

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

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
80
Corey Floyd

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

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

Это очень распространенная модель.

39
Hot Licks

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

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

38
benzado

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

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

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

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

[namesArray filterUsingPredicate:caseInsensitiveBNames];

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

28

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

(1) removeObjectAtIndex: - 271 мс

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

(3) removeObjects: - 326 мс

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

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

18
user1032657

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

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

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

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

17
Jens Ayton

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

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

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
vikingosegundo

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

[theArray filterUsingPredicate:aPredicate]

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

8
Ivan Leider

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

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

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

6
Matjan

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

5
Nathan Kinsinger

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

    NSMutableArray* myArray = ....;

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

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

5
Pokot0

Хорошей реализацией может быть использование метода категории ниже в 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;
}];
1
Werner Altewischer

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

1
Paul Croarkin

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

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

1
Leonardo Constantino

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

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

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

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

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

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

1
Kaiser

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

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

под 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];
0
aremvee