Quando utilizzare enumerateObjectsUsingBlock vs. per


150

Oltre alle ovvie differenze:

  • Utilizzare enumerateObjectsUsingBlockquando sono necessari sia l'indice che l'oggetto
  • Non usare enumerateObjectsUsingBlockquando è necessario modificare le variabili locali (mi sono sbagliato su questo, vedi la risposta di bbum)

È enumerateObjectsUsingBlockgeneralmente considerato migliore o peggiore quando for (id obj in myArray)funzionerebbe anche? Quali sono i vantaggi / gli svantaggi (ad esempio è più o meno performante)?


1
Mi piace usarlo se ho bisogno dell'indice corrente.
Besi,

Risposte:


350

In definitiva, usa il modello che vuoi usare e viene più naturalmente nel contesto.

Sebbene for(... in ...)sia abbastanza conveniente e sintatticamente breve, enumerateObjectsUsingBlock:ha una serie di funzionalità che possono o meno rivelarsi interessanti:

  • enumerateObjectsUsingBlock:sarà più veloce o più veloce dell'enumerazione rapida ( for(... in ...)utilizza il NSFastEnumerationsupporto per implementare l'enumerazione). L'enumerazione rapida richiede la traduzione da una rappresentazione interna alla rappresentazione per un'enumerazione rapida. C'è un sovraccarico al suo interno. L'enumerazione basata su blocchi consente alla classe di raccolta di enumerare i contenuti con la stessa rapidità con cui si attraversa il formato di archiviazione nativo più veloce. Probabilmente irrilevante per gli array, ma può essere un'enorme differenza per i dizionari.

  • "Non utilizzare enumerateObjectsUsingBlock quando è necessario modificare le variabili locali" - non vero; puoi dichiarare i tuoi locali come __blocke saranno scrivibili nel blocco.

  • enumerateObjectsWithOptions:usingBlock: supporta l'enumerazione simultanea o inversa.

  • con i dizionari, l'enumerazione basata su blocchi è l'unico modo per recuperare la chiave e il valore contemporaneamente.

Personalmente, uso enumerateObjectsUsingBlock:più spesso di for (... in ...), ma - ancora una volta - scelta personale.


16
Wow, molto istruttivo. Vorrei poter accettare entrambe queste risposte, ma vado con Chuck perché risuona un po 'di più con me. Inoltre, ho trovato il tuo blog ( friday.com/bbum/2009/08/29/blocks-tips-tricks ) durante la ricerca di __block e ho imparato ancora di più. Grazie.
Paul Wheeler,

8
Per la cronaca, l'enumerazione basata su blocchi non è sempre "così veloce o più veloce" mikeabdullah.net/slow-block-based-dictionary-enumeration.html
Mike Abdullah,

2
@VanDuTran I blocchi vengono eseguiti su un thread separato solo se si dice che devono essere eseguiti su un thread separato. A meno che tu non usi l'opzione di concorrenza dell'enumerazione, verrà eseguita sullo stesso thread della chiamata effettuata
bbum

2
Nick Lockwood ha fatto un bell'articolo su questo, e sembra che enumerateObjectsUsingBlocksia ancora molto più lento dell'enumerazione rapida per array e set. Mi chiedo perché? iosdevelopertips.com/objective-c/…
Bob Spryn,

2
Sebbene sia una specie di dettaglio di implementazione, in questa risposta dovrebbe essere menzionato tra le differenze tra i due approcci che enumerateObjectsUsingBlockavvolgono ogni invocazione del blocco con un pool di rilascio automatico (almeno da OS X 10.10). Questo spiega la differenza di prestazioni rispetto alla for inquale non lo fa.
Pol

83

Per una semplice enumerazione, usare semplicemente l'enumerazione rapida (ovvero un for…in…ciclo) è l'opzione più idiomatica. Il metodo del blocco potrebbe essere leggermente più veloce, ma nella maggior parte dei casi non ha molta importanza: pochi programmi sono collegati alla CPU e anche in questo caso è raro che il ciclo stesso piuttosto che il calcolo all'interno sia un collo di bottiglia.

Un semplice ciclo legge anche più chiaramente. Ecco la caldaia delle due versioni:

for (id x in y){
}

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

Anche se aggiungi una variabile per tenere traccia dell'indice, il ciclo semplice è più facile da leggere.

Quindi quando dovresti usare enumerateObjectsUsingBlock:? Quando si memorizza un blocco da eseguire in seguito o in più punti. È utile quando in realtà si utilizza un blocco come funzione di prima classe piuttosto che come sostituzione eccessiva per un corpo ad anello.


5
enumerateObjectsUsingBlock:sarà la stessa velocità o più veloce dell'enumerazione rapida in tutti i casi. for(... in ...)utilizza l'enumerazione rapida che richiede che la raccolta fornisca una rappresentazione provvisoria delle strutture di dati interne. Come noti, probabilmente irrilevante.
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 I miei test dimostrano che in enumerateObjects...realtà può essere più lenta della rapida enumerazione con un ciclo. Ho eseguito questo test diverse migliaia di volte; il corpo del blocco e ciclo erano uguali singola riga di codice: [(NSOperation *)obj cancel];. Le medie: loop enum veloce - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009e per il blocco - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043. Strano che la differenza di tempo sia così grande e coerente ma, ovviamente, questo è un caso di test molto specifico.
vestito dal

42

Sebbene questa domanda sia vecchia, le cose non sono cambiate, la risposta accettata non è corretta.

L' enumerateObjectsUsingBlockAPI non doveva sostituirsi for-in, ma per un caso d'uso completamente diverso:

  • Consente l'applicazione di logica arbitraria, non locale. cioè non è necessario sapere cosa fa il blocco per usarlo su un array.
  • Enumerazione simultanea per grandi raccolte o calcolo pesante (utilizzando il withOptions:parametro)

L'enumerazione rapida con for-inè ancora il metodo idiomatico di enumerare una raccolta.

L'enumerazione rapida beneficia della brevità del codice, della leggibilità e di ulteriori ottimizzazioni che la rendono innaturalmente veloce. Più veloce di una vecchia C for-loop!

Un rapido test ha concluso che nel 2014 su iOS 7 enumerateObjectsUsingBlockè costantemente più lento del 700% rispetto al for-in (basato su iterazioni di 1 mm di un array di 100 elementi).

Le prestazioni sono una vera preoccupazione pratica qui?

Sicuramente no, con una rara eccezione.

Il punto è quello di dimostrare che c'è poco vantaggio di utilizzare enumerateObjectsUsingBlock:nel corso for-insenza una buona ragione. Non rende il codice più leggibile ... o più veloce ... o sicuro per i thread. (un altro malinteso comune).

La scelta dipende dalle preferenze personali. Per me, l'opzione idiomatica e leggibile vince. In questo caso, si tratta dell'enumerazione rapida utilizzando for-in.

Prova delle prestazioni:

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

risultati:

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

4
Posso confermare che l'esecuzione dello stesso test su un MacBook Pro Retina 2014 enumerateObjectsUsingBlockè in realtà 5 volte più lenta. Ciò è apparentemente dovuto a un pool di rilascio automatico che avvolge ogni invocazione del blocco, cosa che non accade nel for incaso.
Pol

2
Confermo che enumerateObjectsUsingBlock:è ancora 4 volte più lento sul vero iPhone 6 iOS9, usando Xcode 7.x per costruire.
Cœur il

1
Grazie per la conferma! Vorrei che questa risposta non fosse stata così sepolta ... ad alcune persone piace solo la enumeratesintassi perché sembra FP e non vuole ascoltare l'analisi delle prestazioni.
Adam Kaplan,

1
A proposito, non è solo dovuto al pool di autorelease. C'è molto più stack che spinge e esplode conenumerate:
Adam Kaplan

24

Per rispondere alla domanda sulle prestazioni, ho effettuato alcuni test utilizzando il mio progetto di test delle prestazioni . Volevo sapere quale delle tre opzioni per l'invio di un messaggio a tutti gli oggetti in un array è la più veloce.

Le opzioni erano:

1) makeObjectsPerformSelector

[arr makeObjectsPerformSelector:@selector(_stubMethod)];

2) enumerazione rapida e invio regolare di messaggi

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

3) enumerateObjectsUsingBlock e invio regolare di messaggi

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

Si scopre che makeObjectsPerformSelector è stato di gran lunga il più lento. Ci è voluto il doppio della enumerazione veloce. E enumerareObjectsUsingBlock era il più veloce, era circa il 15-20% più veloce dell'iterazione veloce.

Quindi, se sei molto preoccupato per le migliori prestazioni possibili, usa enumerateObjectsUsingBlock. Ma tieni presente che in alcuni casi il tempo necessario per enumerare una raccolta è ridotto dal tempo necessario per eseguire qualsiasi codice tu voglia eseguire ogni oggetto.


Puoi indicare il tuo test specifico? Sembra che tu abbia dato una risposta sbagliata.
Cœur il

3

È abbastanza utile usare enumerateObjectsUsingBlock come loop esterno quando si desidera interrompere i loop nidificati.

per esempio

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

L'alternativa sta usando le dichiarazioni goto.


1
Oppure potresti semplicemente tornare dal metodo proprio come hai fatto qui :-D
Adam Kaplan

1

Grazie a @bbum e @Chuck per aver avviato confronti completi sulle prestazioni. Sono contento di sapere che è banale. Mi sembra di essere andato con:

  • for (... in ...)- come mio goto predefinito. Più intuitivo per me, più cronologia di programmazione qui rispetto a qualsiasi preferenza reale: riutilizzo di più lingue, meno digitazione per la maggior parte delle strutture di dati grazie al completamento automatico dell'IDE: P.

  • enumerateObject...- quando è necessario l'accesso all'oggetto e all'indice. E quando si accede a strutture non array o di dizionari (preferenza personale)

  • for (int i=idx; i<count; i++) - per array, quando devo iniziare con un indice diverso da zero

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.