Wann soll enumerateObjectsUsingBlock vs. für verwendet werden?

150

Neben den offensichtlichen Unterschieden:

  • Verwenden enumerateObjectsUsingBlockSie diese Option, wenn Sie sowohl den Index als auch das Objekt benötigen
  • Nicht verwenden, enumerateObjectsUsingBlockwenn Sie lokale Variablen ändern müssen (ich habe mich geirrt, siehe bbums Antwort)

Wird enumerateObjectsUsingBlockallgemein als besser oder schlechter angesehen, wann for (id obj in myArray)auch funktionieren würde? Was sind die Vor- und Nachteile (zum Beispiel ist es mehr oder weniger performant)?

Paul Wheeler
quelle
1
Ich benutze es gerne, wenn ich den aktuellen Index brauche.
Besi
Siehe auch: stackoverflow.com/questions/8509662/…
Simon Whitaker

Antworten:

349

Verwenden Sie letztendlich das Muster, das Sie verwenden möchten, und kommen Sie im Kontext natürlicher vor.

Obwohl for(... in ...)es recht praktisch und syntaktisch kurz ist, enumerateObjectsUsingBlock:hat es eine Reihe von Funktionen, die sich als interessant erweisen können oder nicht:

  • enumerateObjectsUsingBlock:wird genauso schnell oder schneller sein als die schnelle Aufzählung ( for(... in ...)verwendet die NSFastEnumerationUnterstützung, um die Aufzählung zu implementieren). Eine schnelle Aufzählung erfordert eine Übersetzung von einer internen Darstellung in die Darstellung für eine schnelle Aufzählung. Es gibt Overhead darin. Durch die blockbasierte Aufzählung kann die Auflistungsklasse Inhalte so schnell auflisten wie die schnellste Durchquerung des nativen Speicherformats. Wahrscheinlich irrelevant für Arrays, aber für Wörterbücher kann es ein großer Unterschied sein.

  • "Verwenden Sie enumerateObjectsUsingBlock nicht, wenn Sie lokale Variablen ändern müssen" - nicht wahr; Sie können Ihre Einheimischen als deklarieren __blockund sie werden im Block beschreibbar sein.

  • enumerateObjectsWithOptions:usingBlock: unterstützt entweder gleichzeitige oder umgekehrte Aufzählung.

  • Bei Wörterbüchern ist die blockbasierte Aufzählung die einzige Möglichkeit, Schlüssel und Wert gleichzeitig abzurufen.

Persönlich benutze ich enumerateObjectsUsingBlock:öfter als for (... in ...), aber - wieder - persönliche Wahl.

bbum
quelle
16
Wow, sehr informativ. Ich wünschte, ich könnte diese beiden Antworten akzeptieren, aber ich gehe mit Chuck, weil es ein bisschen mehr bei mir ankommt. Außerdem habe ich Ihr Blog ( friday.com/bbum/2009/08/29/blocks-tips-tricks ) bei der Suche nach __block gefunden und noch mehr gelernt. Danke dir.
Paul Wheeler
8
Für die Aufzeichnung ist blockbasierte Aufzählung nicht immer "so schnell oder schneller" mikeabdullah.net/slow-block-based-dictionary-enumeration.html
Mike Abdullah
2
@ VanDuTran-Blöcke werden nur in einem separaten Thread ausgeführt, wenn Sie angeben, dass sie in einem separaten Thread ausgeführt werden sollen. Wenn Sie nicht die Parallelitätsoption der Aufzählung verwenden, wird sie im selben Thread ausgeführt, in dem der Aufruf getätigt wurde
bbum
2
Nick Lockwood hat einen wirklich schönen Artikel dazu geschrieben, und es scheint enumerateObjectsUsingBlockimmer noch deutlich langsamer zu sein als die schnelle Aufzählung für Arrays und Sets. Ich wundere mich warum? iosdevelopertips.com/objective-c/…
Bob Spryn
2
Obwohl es sich um eine Art Implementierungsdetail handelt, sollte in dieser Antwort zwischen den Unterschieden zwischen den beiden Ansätzen erwähnt werden, enumerateObjectsUsingBlockdie jeden Aufruf des Blocks mit einem Autorelease-Pool umschließen (mindestens ab OS X 10.10). Dies erklärt den Leistungsunterschied, zu for indem dies nicht der Fall ist.
Pol
83

Für eine einfache Aufzählung ist die einfache Verwendung einer schnellen Aufzählung (dh einer for…in…Schleife) die idiomatischere Option. Die Blockmethode mag geringfügig schneller sein, aber das spielt in den meisten Fällen keine große Rolle - nur wenige Programme sind CPU-gebunden, und selbst dann ist es selten, dass die Schleife selbst und nicht die Berechnung im Inneren ein Engpass ist.

Eine einfache Schleife liest auch deutlicher. Hier ist das Boilerplate der beiden Versionen:

for (id x in y){
}

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

Selbst wenn Sie eine Variable hinzufügen, um den Index zu verfolgen, ist die einfache Schleife leichter zu lesen.

Also wann solltest du verwenden enumerateObjectsUsingBlock:? Wenn Sie einen Block speichern, um ihn später oder an mehreren Stellen auszuführen. Dies ist gut, wenn Sie einen Block tatsächlich als erstklassige Funktion verwenden und nicht als überarbeiteten Ersatz für einen Schleifenkörper.

Futter
quelle
5
enumerateObjectsUsingBlock:ist in allen Fällen entweder gleich schnell oder schneller als die schnelle Aufzählung. for(... in ...)verwendet eine schnelle Aufzählung, bei der die Sammlung eine vorläufige Darstellung der internen Datenstrukturen liefern muss. Wie Sie bemerken, wahrscheinlich irrelevant.
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 Meine eigenen Tests zeigen, dass enumerateObjects...die Aufzählung mit einer Schleife tatsächlich langsamer sein kann als die schnelle. Ich habe diesen Test mehrere tausend Mal durchgeführt. Der Hauptteil des Blocks und der Schleife bestand aus derselben einzigen Codezeile : [(NSOperation *)obj cancel];. Die Durchschnittswerte: schnelle Aufzählungsschleife - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009und für den Block - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043. Seltsam, dass der Zeitunterschied so groß und konsistent ist, aber dies ist offensichtlich ein sehr spezifischer Testfall.
Chown
42

Obwohl diese Frage alt ist, haben sich die Dinge nicht geändert, die die akzeptierte Antwort ist falsch.

Die enumerateObjectsUsingBlockAPI sollte nicht ersetzen for-in, sondern für einen völlig anderen Anwendungsfall:

  • Es ermöglicht die Anwendung beliebiger, nicht lokaler Logik. Das heißt, Sie müssen nicht wissen, was der Block tut, um ihn in einem Array zu verwenden.
  • Gleichzeitige Aufzählung für große Sammlungen oder umfangreiche Berechnungen (unter Verwendung des withOptions:Parameters)

Schnelle Aufzählung mit for-inist immer noch die idiomatische Methode zum Auflisten einer Sammlung.

Die schnelle Aufzählung profitiert von der Kürze des Codes, der Lesbarkeit und zusätzlichen Optimierungen , die ihn unnatürlich schnell machen. Schneller als eine alte C-for-Schleife!

Ein schneller Test kommt zu dem Schluss, dass das Jahr 2014 unter iOS 7 enumerateObjectsUsingBlockdurchweg 700% langsamer ist als das For-In (basierend auf 1-mm-Iterationen eines 100-Elemente-Arrays).

Ist Leistung hier ein echtes praktisches Anliegen?

Auf keinen Fall, mit seltenen Ausnahmen.

Der Punkt ist zu zeigen, dass die Verwendung von enumerateObjectsUsingBlock:Over for-inohne wirklich guten Grund wenig Nutzen bringt. Es macht den Code nicht lesbarer ... oder schneller ... oder threadsicherer. (ein weiteres häufiges Missverständnis).

Die Wahl hängt von den persönlichen Vorlieben ab. Für mich gewinnt die idiomatische und lesbare Option. In diesem Fall ist dies die schnelle Aufzählung mit for-in.

Benchmark:

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);

Ergebnisse:

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
quelle
4
Ich kann bestätigen, dass das Ausführen des gleichen Tests auf einem MacBook Pro Retina 2014 enumerateObjectsUsingBlocktatsächlich 5-mal langsamer ist. Dies liegt anscheinend an einem Autorelease-Pool, der jeden Aufruf des Blocks umschließt, was in diesem Fall nicht der for inFall ist.
Pol
2
Ich bestätige, dass enumerateObjectsUsingBlock:es auf echtem iPhone 6 iOS9 immer noch 4-mal langsamer ist und Xcode 7.x zum Erstellen verwendet.
Cœur
1
Danke für die Bestätigung! Ich wünschte, diese Antwort wäre nicht so vergraben worden ... manche Leute mögen die enumerateSyntax einfach, weil sie sich FP-artig anfühlt und keine Leistungsanalyse hören möchte.
Adam Kaplan
1
Übrigens liegt es nicht nur am Autorelease-Pool. Es gibt viel mehr Stack Pushing & Popping mitenumerate:
Adam Kaplan
24

Um die Frage nach der Leistung zu beantworten, habe ich einige Tests mit meinem Leistungstestprojekt durchgeführt . Ich wollte wissen, welche der drei Optionen zum Senden einer Nachricht an alle Objekte in einem Array die schnellste ist.

Die Optionen waren:

1) makeObjectsPerformSelector

[arr makeObjectsPerformSelector:@selector(_stubMethod)];

2) schnelle Aufzählung und reguläres Senden von Nachrichten

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

3) enumerateObjectsUsingBlock & reguläres Senden von Nachrichten

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

Es stellt sich heraus, dass makeObjectsPerformSelector bei weitem am langsamsten war. Die schnelle Aufzählung dauerte doppelt so lange. Und enumerateObjectsUsingBlock war am schnellsten, es war ungefähr 15-20% schneller als schnelle Iteration.

Wenn Sie sich also große Sorgen um die bestmögliche Leistung machen, verwenden Sie enumerateObjectsUsingBlock. Beachten Sie jedoch, dass in einigen Fällen die Zeit, die zum Auflisten einer Sammlung benötigt wird, durch die Zeit, die zum Ausführen des Codes benötigt wird, den jedes Objekt ausführen soll, in den Schatten gestellt wird.

LearnCocos2D
quelle
Können Sie auf Ihren spezifischen Test hinweisen? Sie scheinen eine falsche Antwort gegeben zu haben.
Cœur
3

Es ist ziemlich nützlich, enumerateObjectsUsingBlock als äußere Schleife zu verwenden, wenn Sie verschachtelte Schleifen unterbrechen möchten.

z.B

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

Die Alternative ist die Verwendung von goto-Anweisungen.

Gabe
quelle
1
Oder Sie könnten einfach von der Methode zurückkehren, wie Sie es hier getan haben :-D
Adam Kaplan
1

Vielen Dank an @bbum und @Chuck für den Start umfassender Leistungsvergleiche. Ich bin froh zu wissen, dass es trivial ist. Ich scheine gegangen zu sein mit:

  • for (... in ...)- als meine Standardeinstellung. Intuitiver für mich, mehr Programmierverlauf als jede echte Präferenz - sprachübergreifende Wiederverwendung, weniger Eingabe für die meisten Datenstrukturen aufgrund der automatischen Vervollständigung der IDE: P.

  • enumerateObject...- wenn Zugriff auf Objekt und Index benötigt wird. Und beim Zugriff auf Nicht-Array- oder Wörterbuchstrukturen (persönliche Präferenz)

  • for (int i=idx; i<count; i++) - für Arrays, wenn ich mit einem Index ungleich Null beginnen muss

schneebedeckt
quelle