Kiedy używać enumerateObjectsUsingBlock, a kiedy for

150

Oprócz oczywistych różnic:

  • Użyj, enumerateObjectsUsingBlockgdy potrzebujesz zarówno indeksu, jak i obiektu
  • Nie używaj, enumerateObjectsUsingBlockgdy musisz zmodyfikować zmienne lokalne (myliłem się, zobacz odpowiedź bbum)

Czy enumerateObjectsUsingBlockogólnie uważa się, że lepiej czy gorzej, kiedy for (id obj in myArray)również zadziała? Jakie są zalety / wady (na przykład czy jest bardziej lub mniej wydajne)?

Paul Wheeler
źródło
1
Lubię go używać, jeśli potrzebuję aktualnego indeksu.
Besi
Zobacz też: stackoverflow.com/questions/8509662/…
Simon Whitaker

Odpowiedzi:

350

Ostatecznie użyj dowolnego wzoru, którego chcesz użyć i jest bardziej naturalny w kontekście.

Chociaż for(... in ...)jest dość wygodny i krótki składniowo, enumerateObjectsUsingBlock:ma wiele funkcji, które mogą okazać się interesujące lub nie:

  • enumerateObjectsUsingBlock:będzie tak samo szybkie lub szybsze niż szybkie wyliczanie ( for(... in ...)używa NSFastEnumerationwsparcia do implementacji wyliczania). Szybkie wyliczanie wymaga tłumaczenia z reprezentacji wewnętrznej na reprezentację w celu szybkiego wyliczenia. Jest w tym nad głową. Wyliczanie oparte na blokach umożliwia klasie kolekcji wyliczanie zawartości tak szybko, jak najszybsze przechodzenie w rodzimym formacie magazynu. Prawdopodobnie nie ma to znaczenia w przypadku tablic, ale w przypadku słowników może to być ogromna różnica.

  • „Nie używaj enumerateObjectsUsingBlock, gdy musisz zmodyfikować zmienne lokalne” - nie prawda; możesz zadeklarować swoje lokalne adresy jako __blocki będą dostępne do zapisu w bloku.

  • enumerateObjectsWithOptions:usingBlock: obsługuje wyliczanie równoczesne lub odwrotne.

  • W przypadku słowników wyliczanie oparte na blokach jest jedynym sposobem jednoczesnego pobrania klucza i wartości.

Osobiście używam enumerateObjectsUsingBlock:częściej niż for (... in ...), ale - znowu - osobistego wyboru.

bbum
źródło
16
Wow, bardzo pouczające. Chciałbym móc zaakceptować obie te odpowiedzi, ale wybieram odpowiedź Chucka, ponieważ trochę bardziej do mnie rezonuje. Znalazłem również Twojego bloga ( friday.com/bbum/2009/08/29/blocks-tips-tricks ) podczas wyszukiwania __block i dowiedziałem się jeszcze więcej. Dziękuję Ci.
Paul Wheeler,
8
Dla przypomnienia,
Mike Abdullah
2
Bloki @VanDuTran są wykonywane tylko w osobnym wątku, jeśli powiesz, że mają być wykonywane w oddzielnym wątku. O ile nie użyjesz opcji współbieżności wyliczenia, zostanie ono wykonane w tym samym wątku, w którym zostało wykonane wywołanie
bbum
2
Nick Lockwood napisał naprawdę fajny artykuł na ten temat i wydaje się, że enumerateObjectsUsingBlockjest on nadal znacznie wolniejszy niż szybkie wyliczanie tablic i zestawów. Zastanawiam się dlaczego? iosdevelopertips.com/objective-c/…
Bob Spryn
2
Chociaż jest to rodzaj szczegółu implementacyjnego, należy go wspomnieć w tej odpowiedzi wśród różnic między dwoma podejściami, które enumerateObjectsUsingBlockzawijają każde wywołanie bloku pulą autorelease (przynajmniej od OS X 10.10). To wyjaśnia różnicę w wydajności w porównaniu do tego, for inktóry tego nie robi.
Pol
83

W przypadku prostego wyliczania, po prostu użycie szybkiego wyliczania (tj. for…in…Pętli) jest bardziej idiomatyczną opcją. Metoda blokowa może być nieznacznie szybsza, ale w większości przypadków nie ma to większego znaczenia - niewiele programów jest związanych z procesorem, a nawet wtedy rzadko zdarza się, że sama pętla zamiast obliczeń wewnątrz będzie wąskim gardłem.

Prosta pętla również czyta wyraźniej. Oto schemat dwóch wersji:

for (id x in y){
}

[y enumerateObjectsUsingBlock:^(id x, NSUInteger index, BOOL *stop){
}];

Nawet jeśli dodasz zmienną do śledzenia indeksu, prosta pętla będzie łatwiejsza do odczytania.

Kiedy więc powinieneś użyć enumerateObjectsUsingBlock:? Kiedy przechowujesz blok do wykonania później lub w wielu miejscach. Jest to dobre rozwiązanie, gdy w rzeczywistości używasz bloku jako funkcji pierwszej klasy, a nie zastępczego zamiennika ciała pętli.

Głaskanie pod brodę
źródło
5
enumerateObjectsUsingBlock:we wszystkich przypadkach będzie albo taka sama prędkość, albo szybsza niż szybkie wyliczanie. for(... in ...)wykorzystuje szybkie wyliczanie, które wymaga, aby kolekcja zapewniała tymczasową reprezentację wewnętrznych struktur danych. Jak zauważyłeś, prawdopodobnie nieistotne.
bbum
4
+1When you're storing a block to execute later or in multiple places. It's good for when you're actually using a block as a first-class function rather than an overwrought replacement for a loop body.
Steve
7
@bbum Moje własne testy pokazują, że w enumerateObjects...rzeczywistości może być wolniejsze niż szybkie wyliczanie z pętlą. Przeprowadziłem ten test kilka tysięcy razy; Ciało bloku i pętlą były takie same jednej linii kodu: [(NSOperation *)obj cancel];. Średnie: szybka pętla wyliczeniowa - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009i dla bloku - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043. Dziwne, że różnica czasu jest tak duża i spójna, ale oczywiście jest to bardzo specyficzny przypadek testowy.
chown
42

Chociaż to pytanie jest stare, nic się nie zmieniło, przyjęta odpowiedź jest nieprawidłowa.

enumerateObjectsUsingBlockAPI nie miało zastępują for-in, ale na zupełnie innym przypadku użycia:

  • Pozwala na zastosowanie dowolnej, nielokalnej logiki. tj. nie musisz wiedzieć, co robi blok, aby użyć go na tablicy.
  • Równoczesne wyliczanie dla dużych kolekcji lub ciężkich obliczeń (przy użyciu withOptions:parametru)

Fast Enumeration with for-injest nadal idiomatyczną metodą wyliczania kolekcji.

Fast Enumeration korzysta ze zwięzłości kodu, czytelności i dodatkowych optymalizacji, które sprawiają, że jest nienaturalnie szybki. Szybszy niż stara pętla for w C!

Szybki test stwierdza, że ​​w roku 2014 na iOS 7 enumerateObjectsUsingBlockjest konsekwentnie o 700% wolniejszy niż for-in (na podstawie 1 mm iteracji tablicy 100 elementów).

Czy wydajność jest tutaj praktycznym problemem?

Zdecydowanie nie, z rzadkimi wyjątkami.

Chodzi o to, aby wykazać, że istnieje niewiele korzyści przy użyciu enumerateObjectsUsingBlock:ponad for-inbez naprawdę dobrego powodu. Nie czyni kodu bardziej czytelnym ... ani szybszym ... ani bezpiecznym wątkowo. (kolejne powszechne nieporozumienie).

Wybór sprowadza się do osobistych preferencji. U mnie wygrywa opcja idiomatyczna i czytelna. W tym przypadku jest to szybkie wyliczanie przy użyciu for-in.

Reper:

NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < 100; i++) {
    arr[i] = [NSString stringWithFormat:@"%d", i];
}
int i;
__block NSUInteger length;

i = 1000 * 1000;
uint64_t a1 = mach_absolute_time();
while (--i > 0) {
    for (NSString *s in arr) {
        length = s.length;
    }
}
NSLog(@"For-in %llu", mach_absolute_time()-a1);

i = 1000 * 1000;
uint64_t b1 = mach_absolute_time();
while (--i > 0) {
    [arr enumerateObjectsUsingBlock:^(NSString *s, NSUInteger idx, BOOL *stop) {
        length = s.length;
    }];
}
NSLog(@"Enum %llu", mach_absolute_time()-b1);

Wyniki:

2014-06-11 14:37:47.717 Test[57483:60b] For-in 1087754062
2014-06-11 14:37:55.492 Test[57483:60b] Enum   7775447746
Adam Kaplan
źródło
4
Mogę potwierdzić, że uruchomienie tego samego testu na MacBooku Pro Retina 2014 enumerateObjectsUsingBlockjest w rzeczywistości 5X wolniejsze. Wynika to najwyraźniej z powodu puli autowydzielania opakowującej każde wywołanie bloku, co nie ma miejsca w tym for inprzypadku.
Pol
2
Potwierdzam, że enumerateObjectsUsingBlock:jest nadal 4X wolniejszy na prawdziwym iPhonie 6 iOS9, używając Xcode 7.x do kompilacji.
Cœur
1
Dziękuję za potwierdzenie! Żałuję, że ta odpowiedź nie została tak zagłębiona ... niektórzy ludzie po prostu lubią enumerateskładnię, ponieważ jest podobna do FP i nie chcą słuchać analizy wykonania.
Adam Kaplan,
1
Nawiasem mówiąc, nie chodzi tylko o pulę autowydzielania. Jest dużo więcej pushów i poppingów zenumerate:
Adam Kaplan
24

Aby odpowiedzieć na pytanie dotyczące wydajności, wykonałem kilka testów, korzystając z mojego projektu testów wydajności . Chciałem wiedzieć, która z trzech opcji wysyłania wiadomości do wszystkich obiektów w tablicy jest najszybsza.

Dostępne opcje to:

1) makeObjectsPerformSelector

[arr makeObjectsPerformSelector:@selector(_stubMethod)];

2) szybkie wyliczanie i regularne wysyłanie wiadomości

for (id item in arr)
{
    [item _stubMethod];
}

3) enumerateObjectsUsingBlock i zwykłe wysyłanie wiadomości

[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
 {
     [obj _stubMethod];
 }];

Okazuje się, że makeObjectsPerformSelector był zdecydowanie najwolniejszy. Zajęło to dwa razy więcej czasu niż szybkie wyliczenie. A enumerateObjectsUsingBlock był najszybszy, był około 15-20% szybszy niż szybka iteracja.

Więc jeśli bardzo martwisz się o najlepszą możliwą wydajność, użyj enumerateObjectsUsingBlock. Należy jednak pamiętać, że w niektórych przypadkach czas potrzebny do wyliczenia kolekcji skraca się o czas potrzebny do uruchomienia dowolnego kodu, który ma wykonać każdy obiekt.

LearnCocos2D
źródło
Czy możesz wskazać na swój konkretny test? Wydaje się, że podałeś złą odpowiedź.
Coœur
3

Używanie enumerateObjectsUsingBlock jako zewnętrznej pętli jest dość przydatne, gdy chcesz przerwać zagnieżdżone pętle.

na przykład

[array1 enumerateObjectsUsingBlock:^(id obj1, NSUInteger idx, BOOL * _Nonnull stop) {
  for(id obj2 in array2) {
    for(id obj3 in array3) {
      if(condition) {
        // break ALL the loops!
        *stop = YES;
        return;
      }
    }
  }
}];

Alternatywą jest użycie instrukcji goto.

Gabe
źródło
1
Lub możesz po prostu wrócić z metody, tak jak tutaj :-D
Adam Kaplan
1

Podziękowania dla @bbum i @Chuck za rozpoczęcie wszechstronnych porównań wydajności. Cieszę się, że to trywialne. Wydaje mi się, że poszedłem z:

  • for (... in ...)- jako moje domyślne goto. Bardziej intuicyjny dla mnie, więcej historii programowania niż jakiekolwiek rzeczywiste preferencje - ponowne użycie w różnych językach, mniej pisania dla większości struktur danych dzięki automatycznemu uzupełnianiu IDE: P.

  • enumerateObject...- kiedy potrzebny jest dostęp do obiektu i indeksu. Podczas uzyskiwania dostępu do struktur innych niż tablice lub słownikowe (osobiste preferencje)

  • for (int i=idx; i<count; i++) - dla tablic, kiedy muszę zacząć od niezerowego indeksu

śnieg
źródło