Najlepszy sposób na usunięcie z NSMutableArray podczas iteracji?


194

W Cocoa, jeśli chcę przejść przez NSMutableArray i usunąć wiele obiektów spełniających określone kryteria, jaki jest najlepszy sposób, aby to zrobić bez ponownego uruchamiania pętli za każdym razem, gdy usuwam obiekt?

Dzięki,

Edycja: Tylko dla wyjaśnienia - szukałem najlepszego sposobu, np. Czegoś bardziej eleganckiego niż ręczne aktualizowanie indeksu, w którym jestem. Na przykład w C ++ mogę zrobić;

iterator it = someList.begin();

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

9
Pętla od tyłu do przodu.
Hot Licks

Nikt nie odpowiada na pytanie „DLACZEGO”
onmyway133,

1
@HotLicks Jeden z moich ulubionych i najbardziej niedocenione rozwiązanie w programowaniu: D
Julian F. Weinert

Odpowiedzi:


388

Dla jasności lubię tworzyć początkową pętlę, w której zbieram elementy do usunięcia. Potem je usuwam. Oto przykład z użyciem składni Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Nie ma zatem wątpliwości, czy indeksy są aktualizowane poprawnie, czy też inne szczegóły dotyczące księgowości.

Edytowano, aby dodać:

W innych odpowiedziach zauważono, że odwrotne sformułowanie powinno być szybsze. tzn. jeśli wykonasz iterację przez tablicę i skomponujesz nową tablicę obiektów do zachowania, zamiast obiektów do odrzucenia. To może być prawda (choć co z pamięcią i kosztami przetwarzania przydzielania nowej tablicy i odrzucania starej?), Ale nawet jeśli jest szybsza, może nie być tak duża, jak w przypadku naiwnej implementacji, ponieważ NSArrays nie zachowuj się jak „normalne” tablice. Mówią, ale idą inną drogą.Zobacz dobrą analizę tutaj:

Odwrotne sformułowanie może być szybsze, ale nigdy nie musiałem się tym przejmować, ponieważ powyższy preparat zawsze był wystarczająco szybki dla moich potrzeb.

Dla mnie przesłaniem do domu jest użycie tego, co jest dla ciebie jasne. Zoptymalizuj tylko w razie potrzeby. Osobiście uważam powyższe sformułowanie za najbardziej zrozumiałe i dlatego go używam. Ale jeśli odwrotne sformułowanie jest dla ciebie wyraźniejsze, idź do niego.


47
Uwaga: może to powodować błędy, jeśli obiekty są więcej niż jeden raz w tablicy. Alternatywnie możesz użyć NSMutableIndexSet i - (void) removeObjectsAtIndexes.
Georg Schölly,

1
W rzeczywistości jest szybciej wykonać odwrotność. Różnica w wydajności jest dość duża, ale jeśli twoja tablica nie jest tak duża, czasy i tak będą trywialne.
user1032657

Użyłem odwracania ... stworzyłem tablicę jako zero ... następnie dodałem tylko to, czego chciałem i przeładowałem dane ... zrobiłem ... stworzyłem dummyArray, który jest lustrem głównej tablicy, dzięki czemu mamy główne rzeczywiste dane
Fahim Parkar

Dodatkowa pamięć musi zostać przydzielona do wykonania operacji odwrotnej. To nie jest algorytm lokalny i nie jest dobrym pomysłem w niektórych przypadkach. Kiedy usuwasz bezpośrednio, po prostu przesuwasz tablicę (co jest bardzo wydajne, ponieważ nie przesuwa się jeden po drugim, może przesunąć dużą porcję), ale podczas tworzenia nowej tablicy potrzebujesz całego przydziału i przypisania. Więc jeśli usuniesz tylko kilka elementów, odwrotność będzie znacznie wolniejsza. Jest to typowy algorytm, który wydaje się odpowiedni.
jack

82

Jeszcze jedna odmiana. Dzięki temu zyskujesz czytelność i dobrą wydajność:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

To jest niesamowite. Próbowałem usunąć wiele elementów z tablicy i to zadziałało idealnie. Inne metody powodowały problemy :) Dzięki stary
AbhinavVinay

Corey, ta odpowiedź (poniżej) powiedziała, że removeObjectsAtIndexesto najgorsza metoda na usunięcie obiektów, zgadzasz się z tym? Proszę o to, ponieważ twoja odpowiedź jest już za stara. Nadal dobrze wybrać najlepszy?
Hemang

Bardzo podoba mi się to rozwiązanie pod względem czytelności. Jak to jest pod względem wydajności w porównaniu do odpowiedzi @HotLicks?
Victor Maia Aldecôa

Należy zdecydowanie zachęcać do tej metody podczas usuwania obiektów z tablicy w Obj-C.
boreas

enumerateObjectsUsingBlock:dostaniesz przyrost indeksu za darmo.
pkamb

42

To bardzo prosty problem. Po prostu iterujesz wstecz:

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

Jest to bardzo powszechny wzór.


nie zauważyłby odpowiedzi Jensa, ponieważ nie napisał kodu: P .. thnx m8
FlowUI. SimpleUITesting.com

3
To najlepsza metoda. Ważne jest, aby pamiętać, że zmienna iteracyjna powinna być zmienną podpisaną, mimo że indeksy tablic w Objective-C są zadeklarowane jako niepodpisane (mogę sobie wyobrazić, jak Apple tego teraz żałuje).
mojuba,

39

Niektóre inne odpowiedzi miałyby niską wydajność na bardzo dużych tablicach, ponieważ metody takie jak removeObject:i removeObjectsInArray:polegają na przeprowadzaniu liniowego przeszukiwania odbiornika, co jest marnotrawstwem, ponieważ już wiesz, gdzie jest obiekt. Ponadto każde wywołanie do removeObjectAtIndex:będzie musiało kopiować wartości z indeksu na koniec tablicy w górę o jedno miejsce na raz.

Bardziej wydajne byłyby następujące:

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

Ponieważ ustawiamy pojemność itemsToKeep, nie marnujemy czasu na kopiowanie wartości podczas zmiany rozmiaru. Nie modyfikujemy tablicy w miejscu, więc możemy swobodnie korzystać z szybkiego wyliczania. Korzystanie setArray:zastąpić zawartość arrayze itemsToKeepbędzie skuteczny. W zależności od kodu możesz nawet zamienić ostatni wiersz na:

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

Więc nie ma nawet potrzeby kopiowania wartości, wystarczy zamienić wskaźnik.


Ten algo okazuje się być dobry pod względem złożoności czasowej. Staram się to zrozumieć pod względem złożoności przestrzeni. Mam tę samą implementację, w której ustawiam pojemność dla „itemsToKeep” z faktyczną „Array count”. Ale powiedz, że nie chcę kilku obiektów z tablicy, więc nie dodam ich do itemsToKeep. Tak więc pojemność moich przedmiotówToKeep wynosi 10, ale w rzeczywistości przechowuję tylko 6 obiektów. Czy to oznacza, że ​​marnuję miejsce na 4 przedmioty? PS nie znajduję błędów, tylko próbuję zrozumieć złożoność algo. :)
tech_human

Tak, masz miejsce zarezerwowane na cztery wskaźniki, których nie używasz. Pamiętaj, że tablica zawiera tylko wskaźniki, a nie same obiekty, więc dla czterech obiektów oznacza to 16 bajtów (architektura 32-bitowa) lub 32 bajty (64-bit).
benzado

Zauważ, że każde z rozwiązań będzie co najwyżej wymagało 0 dodatkowej przestrzeni (usuwasz elementy na miejscu) lub w najgorszym wypadku podwoi rozmiar oryginalnej tablicy (ponieważ tworzysz kopię tablicy). Ponownie, ponieważ mamy do czynienia ze wskaźnikami, a nie pełnymi kopiami obiektów, w większości przypadków jest to dość tanie.
benzado

Okej, rozumiem. Dzięki Benzado !!
tech_human

Zgodnie z tym postem podpowiedź tablicy arrayWithCapacity: metoda dotycząca rozmiaru tablicy nie jest faktycznie używana.
Kremk

28

Możesz użyć NSpredicate, aby usunąć elementy ze swojej zmiennej tablicy. To nie wymaga pętli.

Na przykład, jeśli masz NSMutableArray nazw, możesz utworzyć predykat taki jak ten:

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

W poniższym wierszu zostanie wyświetlona tablica zawierająca tylko nazwy zaczynające się na b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Jeśli masz problemy z utworzeniem potrzebnych predykatów, skorzystaj z tego linku dla programistów Apple .


3
Nie wymaga widoczności dla pętli. W operacji filtrowania występuje pętla, po prostu jej nie widzisz.
Hot Licks

18

Zrobiłem test wydajności przy użyciu 4 różnych metod. W każdym teście powtarzano wszystkie elementy w tablicy 100 000 elementów i usuwano co piąty element. Wyniki nie różniły się zbytnio z / bez optymalizacji. Zostały one wykonane na iPadzie 4:

(1) removeObjectAtIndex:- 271 ms

(2) removeObjectsAtIndexes:- 1010 ms (ponieważ zbudowanie zestawu indeksów zajmuje ~ 700 ms; w przeciwnym razie jest to w zasadzie to samo, co wywołanie removeObjectAtIndex: dla każdego elementu)

(3) removeObjects:- 326 ms

(4) utwórz nową tablicę z obiektami, które przejdą test - 17 ms

Tak więc tworzenie nowej tablicy jest zdecydowanie najszybsze. Wszystkie pozostałe metody są porównywalne, z wyjątkiem tego, że użycie removeObjectsAtIndexes: będzie gorzej z większą liczbą elementów do usunięcia, ze względu na czas potrzebny na zbudowanie zestawu indeksów.


1
Czy liczyłeś czas na utworzenie nowej tablicy i późniejszą dezalokację. Nie wierzę, że można to zrobić samemu w 17 ms. Plus 75 000 zleceń?
jack

Czas potrzebny na utworzenie i zwolnienie nowej tablicy jest minimalny
1032657

Najwyraźniej nie zmierzyłeś schematu pętli zwrotnej.
Hot Licks

Pierwszy test jest równoważny schematowi pętli zwrotnej.
user1032657

co się stanie, gdy pozostaniesz z nową tablicą, a tablica poprzedniej tablicy zostanie unieważniona? Będziesz musiał skopiować newArray do tego, do czego została nazwana PrevArray (lub zmienić jej nazwę), w przeciwnym razie, w jaki sposób odniesie się do twojego innego kodu?
aremvee,

17

Użyj pętli odliczającej indeksy:

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

lub wykonaj kopię z obiektami, które chcesz zachować.

W szczególności nie używaj for (id object in array)pętli ani NSEnumerator.


3
powinieneś zapisać swoją odpowiedź w kodzie m8. haha prawie tego przegapił.
FlowUI. SimpleUITesting.com

To nie jest w porządku. „for (identyfikator obiektu w tablicy)” jest szybszy niż „for (NSInteger i = array.count - 1; i> = 0; --i)” i nazywa się szybką iteracją. Korzystanie z iteratora jest zdecydowanie szybsze niż indeksowanie.
jack

@jack - Ale jeśli usuniesz element spod iteratora, zwykle wywołujesz spustoszenie. (A „szybka iteracja” nie zawsze jest o wiele szybsza.)
Hot Licks

Odpowiedź pochodzi z 2008 r. Szybka iteracja nie istniała.
Jens Ayton

12

W systemie iOS 4+ lub OS X 10.6+ Apple dodał passingTestserię interfejsów API NSMutableArray, takich jak – indexesOfObjectsPassingTest:. Rozwiązaniem z takim interfejsem API byłoby:

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

12

Obecnie można używać odwróconego wyliczania opartego na blokach. Prosty przykładowy kod:

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

Wynik:

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

inna opcja z tylko jednym wierszem kodu:

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

Czy jest jakaś gwarancja, że ​​tablica nie zostanie ponownie przydzielona?
Por.

Co masz na myśli z realokacją?
vikingosegundo

Podczas usuwania elementu ze struktury liniowej może być skuteczne przydzielenie nowego ciągłego bloku pamięci i przeniesienie tam wszystkich elementów. W takim przypadku wszystkie wskaźniki zostaną unieważnione.
Fr.

Ponieważ operacja usuwania nie wiedziałaby, ile elementów zostanie usunięta na końcu, nie byłoby sensu tego robić podczas usuwania. W każdym razie: szczegóły implementacji, musimy ufać Apple, aby napisać rozsądny kod.
vikingosegundo

Po pierwsze nie jest ładniej (bo trzeba przede wszystkim przemyśleć, jak to działa), a po drugie nie jest bezpiecznie refaktoryzować. W końcu skończyłem na klasycznie przyjętym rozwiązaniu, które jest właściwie dość czytelne.
Renetik,

8

W bardziej deklaratywny sposób, w zależności od kryteriów pasujących do elementów do usunięcia, możesz użyć:

[theArray filterUsingPredicate:aPredicate]

@Nathan powinien być bardzo wydajny


6

Oto prosty i czysty sposób. Lubię duplikować moją tablicę bezpośrednio w wywołaniu szybkiego wyliczania:

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

W ten sposób wyliczysz kopię usuwanej tablicy, która zawiera te same obiekty. NSArray przechowuje tylko wskaźniki obiektów, więc jest to całkowicie w porządku pod względem pamięci / wydajności.


2
Lub nawet prościej -for (LineItem *item in self.lineItems.copy)
Alexandre G

5

Dodaj obiekty, które chcesz usunąć, do drugiej tablicy, a po pętli użyj -removeObjectsInArray :.


5

powinno to zrobić:

    NSMutableArray* myArray = ....;

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

mam nadzieję że to pomoże...


Chociaż niekonwencjonalne, iteracja wstecz i usuwanie trwa, gdy staje się czystym i prostym rozwiązaniem. Zwykle jedna z najszybszych metod.
rpetrich

Co jest z tym nie tak? Jest czysty, szybki, czytelny i działa jak urok. Dla mnie wygląda to na najlepszą odpowiedź. Dlaczego ma wynik ujemny? Czy coś mi umyka?
Steph Thirion,

1
@Steph: Pytanie brzmi „coś bardziej eleganckiego niż ręczne aktualizowanie indeksu”.
Steve Madsen

1
O. Brakowało mi części absolutnie nie chcę aktualizować indeksu ręcznie. dzięki Steve. IMO to rozwiązanie jest bardziej eleganckie niż wybrane (nie jest potrzebny tymczasowy układ), więc negatywne głosy przeciwko niemu wydają się niesprawiedliwe.
Steph Thirion

@ Steve: Jeśli sprawdzisz zmiany, ta część została dodana po przesłaniu mojej odpowiedzi ... jeśli nie, odpowiedziałbym: „Iteracja wstecz jest najbardziej eleganckim rozwiązaniem” :). Miłego dnia!
Pokot0

1

Dlaczego nie dodasz obiektów do usunięcia do innego NSMutableArray. Po zakończeniu iteracji możesz usunąć zgromadzone obiekty.


1

Co powiesz na zamianę elementów, które chcesz usunąć, na n-ty element, n-1 element i tak dalej?

Po zakończeniu zmieniasz rozmiar tablicy na „poprzedni rozmiar - liczba zamian”


1

Jeśli wszystkie obiekty w tablicy są unikalne lub chcesz usunąć wszystkie wystąpienia obiektu, gdy zostanie znaleziony, możesz szybko wyliczyć kopię tablicy i użyć [NSMutableArray removeObject:], aby usunąć obiekt z oryginału.

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

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

co się stanie, jeśli oryginalny myArray zostanie zaktualizowany podczas +arrayWithArraywykonywania?
bioffe

1
@bioffe: Masz błąd w kodzie. NSMutableArray nie jest bezpieczny dla wątków, oczekuje się, że będziesz kontrolować dostęp za pomocą blokad. Zobacz tę odpowiedź
dreamlax

1

Powyższa odpowiedź benzado jest tym, co powinieneś zrobić dla preformace. W jednej z moich aplikacji removeObjectsInArray zajęło 1 minutę, samo dodanie do nowej tablicy zajęło 0,023 sekundy.


1

Definiuję kategorię, która pozwala mi filtrować za pomocą bloku, jak poniżej:

@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

które można następnie wykorzystać w następujący sposób:

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

1

Lepszą implementacją może być użycie metody kategorii poniżej na 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

Blok predykatów można zaimplementować w celu przetwarzania każdego obiektu w tablicy. Jeśli predykat zwróci wartość true, obiekt zostanie usunięty.

Przykład tablicy dat do usunięcia wszystkich dat, które leżą w przeszłości:

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

0

Iterowanie wstecz było moim ulubionym od lat, ale przez długi czas nigdy nie spotkałem się z przypadkiem, w którym „najgłębszy” (najwyższy wynik) obiekt został usunięty jako pierwszy. Na chwilę przed przejściem wskaźnika do następnego indeksu nie ma nic i ulega awarii.

Sposób Benzado jest najbliższy temu, co robię teraz, ale nigdy nie zdawałem sobie sprawy, że po każdym usunięciu nastąpi przetasowanie stosu.

pod Xcode 6 to działa

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];
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.