Найкращий спосіб видалити з NSMutableArray під час ітерації?


194

Якщо в какао, якщо я хочу пройти цикл через NSMutableArray та видалити декілька об'єктів, що відповідають певним критеріям, який найкращий спосіб зробити це, не перезавантажуючи цикл кожного разу, коли я видаляю об'єкт?

Дякую,

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

iterator it = someList.begin();

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

9
Петля ззаду вперед.
Гарячі лизи

Ніхто не відповідає "ЧОМУ"
onmyway133

1
@HotLicks Один з моїх улюблених за весь час і найбільш занижене рішення в програмуванні взагалі: D
Julian F. Weinert

Відповіді:


388

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

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

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

Відредаговано, щоб додати:

В інших відповідях зазначалося, що зворотна формулювання повинна бути швидшою. тобто якщо ви повторюєте масив і складаєте новий масив об'єктів, який потрібно зберігати, а не об'єкти для відкидання. Це може бути правдою (хоча як щодо витрат на пам'ять та обробку на виділення нового масиву та відмову від старого?), Але навіть якщо це швидше, це може бути не настільки великою справою, як це було б для наївної реалізації, тому що NSArrays не поводяться як "нормальні" масиви. Вони говорять на розмову, але вони ходять іншою прогулянкою. Дивіться хороший аналіз тут:

Зворотна рецептура може бути швидшою, але мені ніколи не потрібно було дбати, чи є вона, тому що вищевказана рецептура завжди була досить швидкою для моїх потреб.

Для мене повідомлення з дому - використовувати будь-який склад, який вам найбільш зрозумілий. Оптимізуйте лише за потреби. Особисто я вважаю, що вищевказана рецептура є найяснішою, саме тому я її використовую. Але якщо зворотна формулювання вам зрозуміліша, ідіть до цього.


47
Слідкуйте за тим, щоб це могло створювати помилки, якщо об’єкти більше одного разу є в масиві. В якості альтернативи ви можете використовувати NSMutableIndexSet і - (void) deleteObjectsAtIndexes.
Георг Шоллі

1
Насправді швидше зробити обернене. Різниця в продуктивності досить величезна, але якщо ваш масив не такий великий, то часи в будь-якому випадку будуть тривіальними.
користувач1032657

Я використовував реверси ... зробив масив як нульовий .. потім додав лише те, що хотів, і перезавантажив дані ... зроблено ... створив dummyArray, який є дзеркалом основного масиву, щоб у нас були основні фактичні дані
Фахім Паркар

Необхідно виділити додаткову пам'ять, щоб зробити обернену. Це не місцевий алгоритм і не хороша ідея в деяких випадках. Коли ви видаляєте безпосередньо, ви просто зміщуєте масив (що дуже ефективно, оскільки він не рухатиметься один за одним, він може зміщувати великий шматок), але коли ви створюєте новий масив, вам потрібно весь розподіл та призначення. Тож якщо ви видалите лише кілька елементів, обернення буде набагато повільніше. Це типовий, здається, правильний алгоритм.
джек

82

Ще одна варіація. Таким чином ви отримуєте читабельність та хорошу ефективність:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

Це круто. Я намагався видалити кілька елементів з масиву, і це спрацювало чудово. Інші методи спричиняли проблеми :) Дякую людині
АбхінавВінай

Кори, у цій відповіді (нижче) сказано, що removeObjectsAtIndexesце найгірший метод видалення об'єктів, ви згодні з цим? Я запитую це, тому що ваша відповідь зараз занадто стара. Все-таки добре вибирати найкращого?
Hemang

Мені це рішення дуже подобається з точки зору читабельності. Як це з точки зору продуктивності порівняно з відповіддю @HotLicks?
Віктор Майя Альдекоа

Цей метод слід обов'язково заохочувати під час видалення об'єктів з масиву в Obj-C.
бореї

enumerateObjectsUsingBlock:отримаєте вам приріст індексу безкоштовно.
пкемб

42

Це дуже проста проблема. Ви просто повторіть назад:

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

Це дуже поширена закономірність.


пропустив би відповідь Йенса, бо він не
виписав

3
Це найкращий метод. Важливо пам’ятати, що змінна ітерації повинна бути підписаною, незважаючи на те, що індекси масиву в Objective-C оголошуються ненаписаними (я можу уявити, як Apple шкодує про це зараз).
моджуба

39

Деякі інші відповіді будуть мати низьку продуктивність на дуже великих масивів, так як методи , як 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];

Тож навіть не потрібно копіювати значення, лише міняйте покажчик.


Це альго, як виявляється, є складним у часі. Я намагаюся зрозуміти це з точки зору складності простору. У мене така ж реалізація, де я встановлюю ємність для 'itemsToKeep' з фактичним числом масивів '. Але скажіть, що я не хочу небагато об’єктів з масиву, тому я не додаю його до itemsToKeep. Отже, ємність моїх предметівToKeep становить 10, але я фактично зберігаю в ній лише 6 об’єктів. Це означає, що я витрачаю місце на 4 об’єкти? PS я не знаходжу помилок, просто намагаюся зрозуміти складність альго. :)
tech_human

Так, у вас є місце, відведене для чотирьох покажчиків, які ви не використовуєте. Майте на увазі, що масив містить лише вказівники, а не самі об’єкти, тому для чотирьох об’єктів, що означає 16 байт (32-бітова архітектура) або 32 байти (64 біт).
бензадо

Зауважте, що будь-яке з рішень у кращому випадку потребує 0 додаткового місця (ви видаляєте елементи на місці) або в гіршому випадку подвоює вихідний розмір масиву (тому що ви робите копію масиву). Знову ж таки, оскільки ми маємо справу з покажчиками, а не повними копіями об’єктів, це за досить обставин досить дешево.
бензадо

Гаразд зрозумів. Дякую Бензадо !!
tech_human

Відповідно до цієї публікації, натяк на arrayWithCapacity: метод про розмір масиву насправді не використовується.
Кремк

28

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

Наприклад, якщо у вас є NSMutableArray імен, ви можете створити предикат на зразок цього:

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

Наступний рядок залишить вам масив, який містить лише імена, починаючи з b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Якщо у вас виникли проблеми зі створенням потрібних предикатів, скористайтеся цим посиланням для розробників яблук .


3
Не вимагає видимих циклів. У роботі фільтра є цикл, ви просто не бачите його.
Гарячі лизання

18

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

(1) removeObjectAtIndex:- 271 мс

(2) removeObjectsAtIndexes:- 1010 мс (оскільки складання набору індексів займає ~ 700 мс; інакше це в основному те саме, що викликати RemoveObjectAtIndex: для кожного елемента)

(3) removeObjects:- 326 мс

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

Отже, створити новий масив - це найшвидше. Інші методи всі порівнянні, за винятком того, що використання RemoveObjectsAtIndexes: буде гірше, якщо видалити більше елементів, через час, необхідний для складання набору індексів.


1
Ви порахували час, щоб створити новий масив і розібрати його згодом. Я не вірю, що це може зробити це самостійно за 17 мс. Плюс 75 000 завдань?
джек

Час, необхідний для створення та розміщення нового масиву, мінімальний
user1032657,

Ви, мабуть, не вимірювали схему зворотного циклу.
Гарячі лизання

Перший тест еквівалентний схемі зворотного циклу.
користувач1032657

що станеться, коли ти залишишся з новимArray, а твій prevArray обнулений? Вам доведеться скопіювати newArray в те, на що був названий PrevArray (або перейменувати його), інакше як ваш інший код посилатиметься на нього?
aremvee

17

Або використовуйте зворотний відлік циклу за показниками:

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

або зробити копію з об’єктами, які ви хочете зберегти.

Зокрема, не використовуйте for (id object in array)петлю або NSEnumerator.


3
вам слід написати свою відповідь у коді m8. ха-ха це майже не пропустив.
FlowUI. SimpleUITesting.com

Це неправильно. "for (id об'єкта в масиві)" швидше, ніж "for (NSInteger i = array.count - 1; i> = 0; --i)", і називається швидкою ітерацією. Використання ітератора, безумовно, швидше, ніж індексація.
джек

@jack - Але якщо ви вилучите елемент з-під ітератора, ви зазвичай створюєте хаос. (І "швидка ітерація" не завжди буває набагато швидшою.)
Гарячі лизання

Відповідь - з 2008 року. Швидкої ітерації не існувало.
Єнс Айтон

12

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

12

Сьогодні ви можете використовувати зворотне перерахування на основі блоків. Простий приклад коду:

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

Чи є гарантія, що масив не буде перерозподілений?
Cfr

Що ви маєте на увазі під перерозподілом?
vikingosegundo

При видаленні елемента з лінійної структури може бути ефективним виділити новий суміжний блок пам'яті і перемістити всі елементи туди. У цьому випадку всі покажчики будуть визнані недійсними.
Cfr

Оскільки операція видалення не знає, на скільки елементів буде зроблено ставка, видалених наприкінці, це не має сенсу робити під час видалення. У будь-якому разі: деталі реалізації, нам доведеться довіряти Apple, щоб написати розумний код.
vikingosegundo

По-перше, це не приємніше (адже в першу чергу потрібно думати про те, як це працює), а по-друге, це не безпечно. Отже, врешті-решт я закінчився класичним прийнятим рішенням, яке насправді є читабельним.
Ренетик

8

Більш декларативно, залежно від критеріїв, які відповідають елементам для видалення, ви можете використовувати:

[theArray filterUsingPredicate:aPredicate]

@Nathan має бути дуже ефективним


6

Ось простий і чистий спосіб. Мені подобається дублювати масив прямо під час виклику швидкого перерахування:

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

Таким чином ви перераховуєте копію масиву, що видаляється з обох, що містять однакові об'єкти. NSArray утримує лише вказівники об'єкта, так що це абсолютно чудова пам'ять / продуктивність.


2
Або ще простіше -for (LineItem *item in self.lineItems.copy)
Олександр G

5

Додайте об'єкти, які ви хочете видалити, до другого масиву та після циклу використовуйте -removeObjectsInArray :.


5

це слід зробити:

    NSMutableArray* myArray = ....;

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

сподіваюся, що це допоможе ...


Хоча це неортодоксально, я виявив ітерацію назад і видалення, коли я переходжу до чистого і простого рішення. Зазвичай також один з найшвидших методів.
rpetrich

Що з цим? Це чисто, швидко, легко читається, і це працює як шарм. Для мене це виглядає як найкраща відповідь. Чому він має від'ємний бал? Я щось тут пропускаю?
Стеф Тіріон

1
@Steph: У запитанні йдеться про "щось більш елегантне, ніж оновлення індексу вручну".
Стів Мадсен

1
ой. Я пропустив частину «я абсолютно не хочу-оновити-індекс» вручну. спасибі Стів. IMO це рішення є більш елегантним, ніж обраний (не потрібен тимчасовий масив), тому негативні голоси проти нього почуваються несправедливими.
Стеф Тіріон

@Steve: Якщо ви перевірите правки, ця частина була додана після того, як я надіслав свою відповідь .... якщо ні, я б відповів "Ітерація назад - це найелегантніше рішення" :). Гарного дня!
Pokot0

1

Чому б вам не додати об’єкти для видалення до іншого NSMutableArray. Закінчивши ітерацію, ви можете видалити зібрані вами предмети.


1

Як щодо заміни елементів, які ви хочете видалити, елементами 'n'th,' n-1'th тощо?

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


1

Якщо всі об'єкти у вашому масиві унікальні або ви хочете видалити всі виникнення об'єкта при їх знаходженні, ви можете швидко перерахувати копію масиву та використати [NSMutableArray removeObject:] для видалення об'єкта з оригіналу.

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

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

що станеться, якщо оригінальний myArray оновлюється під час +arrayWithArrayйого виконання?
bioffe

1
@bioffe: Тоді у вас є помилка в коді. NSMutableArray не є безпечним для потоків, і, як очікується, ви зможете контролювати доступ через блокування. Дивіться цю відповідь
dreamlax

1

Зверху бензадо - це те, що слід зробити для підготовки. В одному з моїх додатків deleteObjectsInArray зайняв час роботи 1 хвилину, просто додавання до нового масиву займало 0,023 секунди.


1

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

@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

Більш приємною реалізацією може бути використання методу категорії, наведеного нижче на 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

Блок предиката може бути реалізований для виконання обробки кожного об'єкта в масиві. Якщо предикат повертає істину, об'єкт видаляється.

Приклад масиву дат для видалення всіх дат, що були в минулому:

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

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];
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.