Cara terbaik untuk menghapus dari NSMutableArray saat iterasi?

194

Di Cocoa, jika saya ingin mengulang melalui NSMutableArray dan menghapus beberapa objek yang memenuhi kriteria tertentu, apa cara terbaik untuk melakukan ini tanpa memulai ulang loop setiap kali saya menghapus objek?

Terima kasih,

Sunting: Hanya untuk memperjelas - Saya sedang mencari cara terbaik, misalnya sesuatu yang lebih elegan daripada memperbarui secara manual indeks saya. Misalnya di C ++ yang bisa saya lakukan;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
Andrew Grant
sumber
9
Loop dari belakang ke depan.
Hot Licks
Tidak ada yang menjawab "MENGAPA"
onmyway133
1
@HotLicks Salah satu favorit saya sepanjang masa dan solusi yang paling diremehkan dalam pemrograman umumnya: D
Julian F. Weinert

Jawaban:

388

Untuk lebih jelasnya saya ingin membuat loop awal di mana saya mengumpulkan item untuk dihapus. Lalu saya menghapusnya. Berikut ini contoh menggunakan sintaks Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Maka tidak ada pertanyaan tentang apakah indeks diperbarui dengan benar, atau detail pembukuan kecil lainnya.

Diedit untuk menambahkan:

Telah dicatat dalam jawaban lain bahwa formulasi terbalik harus lebih cepat. yaitu Jika Anda beralih melalui array dan menyusun array objek baru untuk disimpan, bukan objek untuk dibuang. Itu mungkin benar (walaupun bagaimana dengan memori dan biaya pemrosesan mengalokasikan array baru, dan membuang yang lama?) Tetapi bahkan jika lebih cepat itu mungkin bukan masalah besar seperti untuk implementasi naif, karena NSArrays jangan berperilaku seperti array "normal". Mereka bicara tetapi mereka berjalan dengan cara yang berbeda. Lihat analisis yang baik di sini:

Formulasi terbalik mungkin lebih cepat, tetapi saya tidak pernah perlu peduli apakah itu, karena formulasi di atas selalu cukup cepat untuk kebutuhan saya.

Bagi saya pesan untuk dibawa pulang adalah menggunakan formulasi apa pun yang paling jelas bagi Anda. Optimalkan hanya jika perlu. Saya pribadi menemukan formulasi di atas paling jelas, itulah sebabnya saya menggunakannya. Tetapi jika formulasi terbalik lebih jelas bagi Anda, lakukanlah.

Christopher Ashworth
sumber
47
Hati-hati karena ini dapat membuat bug jika objek lebih dari satu kali dalam array. Sebagai alternatif, Anda bisa menggunakan NSMutableIndexSet dan - (void) removeObjectsAtIndexes.
Georg Schölly
1
Sebenarnya lebih cepat melakukan invers. Perbedaan kinerja cukup besar, tetapi jika array Anda tidak terlalu besar maka waktunya akan sepele.
user1032657
Saya menggunakan revers ... membuat array sebagai nil .. lalu hanya menambahkan apa yang saya inginkan dan memuat ulang data ... selesai ... membuat dummyArray yang merupakan cermin dari array utama sehingga kami memiliki data aktual utama
Fahim Parkar
Memori tambahan perlu dialokasikan untuk melakukan invers. Ini bukan algoritma di tempat dan bukan ide yang baik dalam beberapa kasus. Ketika Anda menghapus secara langsung, Anda hanya menggeser array (yang sangat efisien karena tidak akan bergerak satu per satu, itu dapat menggeser potongan besar), tetapi ketika Anda membuat array baru Anda membutuhkan semua alokasi dan penugasan. Jadi, jika Anda hanya menghapus beberapa item, kebalikannya akan jauh lebih lambat. Ini adalah algoritma yang tampaknya benar.
jack
82

Satu lagi variasi. Jadi Anda mendapatkan keterbacaan dan kinerja yang baik:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
Corey Floyd
sumber
Ini luar biasa. Saya mencoba untuk menghapus beberapa item dari array dan ini berfungsi dengan baik. Metode lain yang menyebabkan masalah :) Terima kasih
kawan
Corey, jawaban ini (di bawah) mengatakan bahwa, removeObjectsAtIndexesadalah metode terburuk untuk menghapus objek, apakah Anda setuju dengan itu? Saya menanyakan ini karena jawaban Anda terlalu tua sekarang. Tetap bagus memilih yang terbaik?
Hemang
Saya suka solusi ini dalam hal keterbacaan. Bagaimana dalam hal kinerja bila dibandingkan dengan jawaban @HotLicks?
Victor Maia Aldecôa
Metode ini harus didorong saat menghapus objek dari array di Obj-C.
boreas
enumerateObjectsUsingBlock:akan membuat Anda kenaikan indeks gratis.
pkamb
42

Ini adalah masalah yang sangat sederhana. Anda baru saja beralih mundur:

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

Ini adalah pola yang sangat umum.

Hot Licks
sumber
akan melewatkan jawaban Jens, karena dia tidak menulis kode: P .. thnx m8
FlowUI. SimpleUITesting.com
3
Ini adalah metode terbaik. Penting untuk diingat bahwa variabel iterasi haruslah yang bertanda tangan, meskipun indeks array di Objective-C dinyatakan tidak ditandatangani (saya bisa membayangkan bagaimana Apple menyesalinya sekarang).
mojuba
39

Beberapa jawaban lain akan memiliki kinerja yang buruk pada array yang sangat besar, karena metode suka removeObject:dan removeObjectsInArray:melibatkan melakukan pencarian linier pada penerima, yang merupakan pemborosan karena Anda sudah tahu di mana objek tersebut. Selain itu, setiap panggilan ke removeObjectAtIndex:harus menyalin nilai dari indeks ke akhir array dengan satu slot pada suatu waktu.

Yang lebih efisien adalah sebagai berikut:

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

Karena kami mengatur kapasitas itemsToKeep, kami tidak membuang waktu menyalin nilai selama pengubahan ukuran. Kami tidak mengubah array di tempat, jadi kami bebas menggunakan Penghitungan Cepat. Menggunakan setArray:untuk mengganti konten arraydengan itemsToKeepakan efisien. Bergantung pada kode Anda, Anda bahkan dapat mengganti baris terakhir dengan:

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

Jadi bahkan tidak perlu menyalin nilai, hanya menukar pointer.

benzado
sumber
Algo ini terbukti bagus dari segi kompleksitas waktu. Saya mencoba memahaminya dalam hal kompleksitas ruang. Saya memiliki implementasi yang sama di mana saya mengatur kapasitas untuk 'itemsToKeep' dengan 'Array count' yang sebenarnya. Tetapi katakan saya tidak ingin beberapa objek dari array jadi saya tidak menambahkannya ke itemsToKeep. Sehingga kapasitas item saya ToKeep adalah 10, tapi saya sebenarnya hanya menyimpan 6 objek di dalamnya. Apakah ini berarti saya membuang-buang ruang untuk 4 objek? PS tidak menemukan kesalahan, hanya mencoba memahami kompleksitas algo. :)
tech_human
Ya, Anda memiliki ruang yang disediakan untuk empat petunjuk yang tidak Anda gunakan. Perlu diingat array hanya berisi pointer, bukan objek itu sendiri, jadi untuk empat objek yang berarti 16 byte (arsitektur 32 bit) atau 32 byte (64 bit).
benzado
Perhatikan bahwa salah satu solusi yang terbaik membutuhkan 0 ruang tambahan (Anda menghapus item di tempat) atau paling tidak dua kali lipat ukuran array asli (karena Anda membuat salinan array). Sekali lagi, karena kita berurusan dengan pointer dan bukan salinan penuh objek, ini cukup murah di sebagian besar keadaan.
benzado
OK saya mengerti. Terima kasih Benzado !!
tech_human
Menurut ini posting , petunjuk dari arrayWithCapacity: metode tentang ukuran array sebenarnya tidak digunakan.
Kremk
28

Anda dapat menggunakan NSpredicate untuk menghapus item dari array yang bisa berubah-ubah. Ini tidak memerlukan loop.

Misalnya jika Anda memiliki NSMutableArray nama, Anda dapat membuat predikat seperti ini:

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

Baris berikut akan meninggalkan Anda dengan array yang hanya berisi nama yang dimulai dengan b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Jika Anda kesulitan membuat predikat yang Anda butuhkan, gunakan tautan pengembang apel ini .


sumber
3
Tidak perlu terlihat untuk loop. Ada loop dalam operasi filter, Anda tidak melihatnya.
Hot Licks
18

Saya melakukan tes kinerja menggunakan 4 metode berbeda. Setiap tes diulangi melalui semua elemen dalam array elemen 100.000, dan dihapus setiap item ke-5. Hasilnya tidak banyak berbeda dengan / tanpa optimasi. Ini dilakukan pada iPad 4:

(1) removeObjectAtIndex: - 271 ms

(2) removeObjectsAtIndexes: - 1010 ms (karena membangun set indeks membutuhkan ~ 700 ms; jika tidak, ini pada dasarnya sama dengan memanggil removeObjectAtIndex: untuk setiap item)

(3) removeObjects: - 326 ms

(4) membuat array baru dengan objek yang lulus tes - 17 ms

Jadi, membuat array baru adalah yang tercepat. Metode lain semuanya sebanding, kecuali yang menggunakan removeObjectsAtIndexes: akan lebih buruk dengan lebih banyak item untuk dihapus, karena waktu yang dibutuhkan untuk membangun set indeks.

pengguna1032657
sumber
1
Apakah Anda menghitung waktu untuk membuat array baru dan membatalkan alokasi setelahnya. Saya tidak percaya itu bisa melakukannya sendiri dalam 17 ms. Ditambah 75.000 tugas?
jack
Waktu yang diperlukan untuk membuat dan
membatalkan
Anda tampaknya tidak mengukur skema loop terbalik.
Hot Licks
Tes pertama setara dengan skema loop terbalik.
user1032657
apa yang terjadi ketika Anda dibiarkan dengan newArray Anda, dan prevArray Anda Nulled? Anda harus menyalin newArray ke tempat PrevArray dinamai (atau mengganti nama) jika tidak, bagaimana kode Anda yang lain merujuknya?
aremvee
17

Gunakan loop yang menghitung mundur indeks:

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

atau buat salinan dengan objek yang ingin Anda simpan.

Secara khusus, jangan gunakan for (id object in array)loop atau NSEnumerator.

Jens Ayton
sumber
3
Anda harus menuliskan jawaban Anda dalam kode m8. haha hampir melewatkannya.
FlowUI. SimpleUITesting.com
Ini tidak benar. "untuk (objek id dalam array)" lebih cepat dari "untuk (NSInteger i = array.count - 1; i> = 0; --i)", dan disebut iterasi cepat. Menggunakan iterator jelas lebih cepat daripada pengindeksan.
jack
@jack - Tetapi jika Anda menghapus elemen dari bawah iterator Anda biasanya membuat kekacauan. (Dan "iterasi cepat" tidak selalu lebih cepat.)
Hot Licks
Jawabannya adalah dari 2008. Iterasi cepat tidak ada.
Jens Ayton
12

Untuk iOS 4+ atau OS X 10.6+, Apple menambahkan passingTestserangkaian API NSMutableArray, seperti – indexesOfObjectsPassingTest:. Solusi dengan API tersebut adalah:

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

Saat ini Anda dapat menggunakan enumerasi berbasis blok terbalik. Contoh kode sederhana:

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

Hasil:

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

opsi lain hanya dengan satu baris kode:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];
vikingosegundo
sumber
Apakah ada jaminan bahwa array tidak akan dialokasikan kembali?
Cfr
Apa yang Anda maksud dengan realokasi?
vikingosegundo
Saat menghapus elemen dari struktur linear, mungkin efektif untuk mengalokasikan blok memori baru yang berdekatan dan memindahkan semua elemen di sana. Dalam hal ini semua petunjuk akan dibatalkan.
Cfr
Karena operasi penghapusan tidak akan tahu berapa banyak elemen yang akan bertaruh dihapus pada akhirnya, itu tidak masuk akal untuk melakukannya selama penghapusan. Pokoknya: detail implementasi, kita harus percaya apel untuk menulis kode yang masuk akal.
vikingosegundo
Pertama tidak lebih baik (karena Anda harus berpikir banyak tentang cara kerjanya di tempat pertama) dan kedua tidak refactoring aman. Jadi pada akhirnya saya berakhir dengan solusi klasik yang diterima yang sebenarnya cukup mudah dibaca.
Renetik
8

Dengan cara yang lebih deklaratif, tergantung pada kriteria yang cocok dengan item yang ingin Anda hapus dapat digunakan:

[theArray filterUsingPredicate:aPredicate]

@Nathan harusnya sangat efisien

elp
sumber
6

Inilah cara yang mudah dan bersih. Saya suka menduplikasi array saya tepat di panggilan penghitungan cepat:

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

Dengan cara ini Anda menghitung melalui salinan array yang dihapus, keduanya memegang objek yang sama. NSArray menyimpan pointer objek hanya jadi ini benar-benar memori / kinerja bijaksana.

Matjan
sumber
2
Atau bahkan lebih sederhana -for (LineItem *item in self.lineItems.copy)
Alexandre G
5

Tambahkan objek yang ingin Anda hapus ke array kedua dan, setelah loop, gunakan -removeObjectsInArray :.

Nathan Kinsinger
sumber
5

ini harus dilakukan:

    NSMutableArray* myArray = ....;

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

semoga ini membantu...

Pokot0
sumber
Meskipun tidak ortodoks, saya menemukan iterasi mundur dan menghapus ketika saya menjadi solusi yang bersih dan sederhana. Biasanya salah satu metode tercepat juga.
rpetrich
Ada apa dengan ini? Ini bersih, cepat, mudah dibaca, dan itu berfungsi seperti pesona. Bagi saya itu sepertinya jawaban terbaik. Mengapa skornya negatif? Apakah saya melewatkan sesuatu di sini?
Steph Thirion
1
@Steph: Pertanyaannya menyatakan "sesuatu yang lebih elegan daripada memperbarui indeks secara manual."
Steve Madsen
1
oh Saya telah melewatkan bagian I-benar-benar-tidak-ingin-memperbarui-indeks-secara manual. terima kasih steve. IMO solusi ini lebih elegan dari yang dipilih (tidak perlu array sementara), sehingga suara negatif terhadapnya terasa tidak adil.
Steph Thirion
@Steve: Jika Anda memeriksa pengeditan, bagian itu ditambahkan setelah saya mengirimkan jawaban saya .... jika tidak, saya akan menjawab dengan "Iterating mundur ADALAH solusi paling elegan" :). Semoga harimu menyenangkan!
Pokot0
1

Mengapa Anda tidak menambahkan objek yang akan dihapus ke NSMutableArray lain. Setelah selesai iterasi, Anda dapat menghapus objek yang telah Anda kumpulkan.

Paul Croarkin
sumber
1

Bagaimana dengan menukar elemen yang ingin Anda hapus dengan elemen 'n'th,' n-1'th elemen, dan sebagainya?

Setelah selesai, Anda mengubah ukuran array ke 'ukuran sebelumnya - jumlah swap'

Cyber ​​Oliveira
sumber
1

Jika semua objek dalam array Anda adalah unik atau Anda ingin menghapus semua kemunculan objek saat ditemukan, Anda bisa menghitung dengan cepat pada salinan array dan menggunakan [NSMutableArray removeObject:] untuk menghapus objek dari aslinya.

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

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}
lajos
sumber
apa yang terjadi jika myArray asli diperbarui saat +arrayWithArraysedang dijalankan?
bioffe
1
@bioffe: Kemudian Anda memiliki bug dalam kode Anda. NSMutableArray bukan thread-safe, Anda diharapkan untuk mengontrol akses melalui kunci. Lihat jawaban ini
dreamlax
1

Anwser benzado di atas adalah apa yang harus Anda lakukan untuk preformace. Dalam salah satu aplikasi saya, removeObjectsInArray membutuhkan waktu 1 menit, hanya dengan menambahkan array baru, 0,023 detik.

kaisar
sumber
1

Saya mendefinisikan kategori yang memungkinkan saya memfilter menggunakan blok, seperti ini:

@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

yang kemudian bisa digunakan seperti ini:

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

Implementasi yang lebih baik bisa menggunakan metode kategori di bawah ini pada 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 predikat dapat diimplementasikan untuk melakukan pemrosesan pada setiap objek dalam array. Jika predikat mengembalikan true objek dihapus.

Contoh untuk array tanggal untuk menghapus semua tanggal yang ada di masa lalu:

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

Iterating mundur adalah favorit saya selama bertahun-tahun, tetapi untuk waktu yang lama saya tidak pernah menemukan kasus di mana objek 'terdalam' (jumlah tertinggi) pertama kali dihapus. Sesaat sebelum pointer bergerak ke indeks berikutnya tidak ada apa-apa dan itu crash.

Cara Benzado adalah yang paling dekat dengan apa yang saya lakukan sekarang tetapi saya tidak pernah menyadari akan ada perombakan tumpukan setelah setiap pemindahan.

di bawah Xcode 6 ini berfungsi

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];
Aremvee
sumber