Quand utiliser enumerateObjectsUsingBlock ou pour

150

Outre les différences évidentes:

  • À utiliser enumerateObjectsUsingBlocklorsque vous avez besoin à la fois de l'index et de l'objet
  • Ne pas utiliser enumerateObjectsUsingBlocklorsque vous devez modifier des variables locales (je me suis trompé à ce sujet, voir la réponse de bbum)

Est-ce enumerateObjectsUsingBlockgénéralement considéré comme meilleur ou pire quand for (id obj in myArray)fonctionnerait également? Quels sont les avantages / inconvénients (par exemple est-il plus ou moins performant)?

Paul Wheeler
la source
1
J'aime l'utiliser si j'ai besoin de l'index actuel.
Besi
Voir aussi: stackoverflow.com/questions/8509662/…
Simon Whitaker

Réponses:

350

En fin de compte, utilisez le modèle que vous souhaitez utiliser et qui vient plus naturellement dans le contexte.

Bien qu'il for(... in ...)soit assez pratique et syntaxiquement bref, il enumerateObjectsUsingBlock:présente un certain nombre de fonctionnalités qui peuvent ou non s'avérer intéressantes:

  • enumerateObjectsUsingBlock:sera aussi rapide ou plus rapide que l'énumération rapide ( for(... in ...)utilise le NSFastEnumerationsupport pour implémenter l'énumération). L'énumération rapide nécessite la traduction d'une représentation interne vers la représentation pour une énumération rapide. Il y a des frais généraux là-dedans. L'énumération basée sur des blocs permet à la classe de collection d'énumérer le contenu aussi rapidement que le parcours le plus rapide du format de stockage natif. Probablement pas pertinent pour les tableaux, mais cela peut être une énorme différence pour les dictionnaires.

  • "N'utilisez pas enumerateObjectsUsingBlock lorsque vous devez modifier des variables locales" - pas vrai; vous pouvez déclarer vos sections locales comme __blocket elles seront inscriptibles dans le bloc.

  • enumerateObjectsWithOptions:usingBlock: prend en charge l'énumération simultanée ou inversée.

  • avec les dictionnaires, l'énumération par blocs est le seul moyen de récupérer simultanément la clé et la valeur.

Personnellement, j'utilise enumerateObjectsUsingBlock:plus souvent que for (... in ...), mais - encore une fois - choix personnel.

bbum
la source
16
Wow, très instructif. J'aurais aimé pouvoir accepter ces deux réponses, mais je suis d'accord avec celle de Chuck parce qu'elle résonne un peu plus avec moi. Aussi, j'ai trouvé votre blog ( friday.com/bbum/2009/08/29/blocks-tips-tricks ) en cherchant __block et j'en ai appris encore plus. Je vous remercie.
Paul Wheeler
8
Pour mémoire, l'énumération par blocs n'est pas toujours "aussi rapide ou plus rapide" mikeabdullah.net/slow-block-based-dictionary-enumeration.html
Mike Abdullah
2
Les blocs @VanDuTran ne sont exécutés sur un thread séparé que si vous leur dites d'être exécutés sur un thread séparé. À moins que vous n'utilisiez l'option d'énumération de concurrence, alors il sera exécuté sur le même thread que l'appel a été effectué
bbum
2
Nick Lockwood a fait un très bel article à ce sujet, et il semble qu'il enumerateObjectsUsingBlocksoit encore beaucoup plus lent que l'énumération rapide des tableaux et des ensembles. Je me demande pourquoi? iosdevelopertips.com/objective-c/…
Bob Spryn
2
Bien que ce soit une sorte de détail d'implémentation, il convient de le mentionner dans cette réponse parmi les différences entre les deux approches qui enumerateObjectsUsingBlockencapsulent chaque appel du bloc avec un pool de libération automatique (à partir d'OS X 10.10 au moins). Cela explique la différence de performance par rapport à for inqui ne fait pas cela.
Pol
83

Pour une énumération simple, utiliser simplement une énumération rapide (c'est-à-dire une for…in…boucle) est l'option la plus idiomatique. La méthode par blocs peut être légèrement plus rapide, mais cela n'a pas beaucoup d'importance dans la plupart des cas - peu de programmes sont liés au processeur, et même dans ce cas, il est rare que la boucle elle-même plutôt que le calcul à l'intérieur soit un goulot d'étranglement.

Une simple boucle se lit également plus clairement. Voici le passe-partout des deux versions:

for (id x in y){
}

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

Même si vous ajoutez une variable pour suivre l'index, la boucle simple est plus facile à lire.

Alors, quand devriez-vous utiliser enumerateObjectsUsingBlock:? Lorsque vous stockez un bloc à exécuter plus tard ou à plusieurs endroits. C'est bien lorsque vous utilisez réellement un bloc comme fonction de première classe plutôt que comme un remplacement surchargé d'un corps de boucle.

Mandrin
la source
5
enumerateObjectsUsingBlock:sera soit la même vitesse, soit plus rapide que le dénombrement rapide dans tous les cas. for(... in ...)utilise une énumération rapide qui oblige la collection à fournir une représentation intermédiaire des structures de données internes. Comme vous le remarquez, probablement hors de propos.
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 Mes propres tests montrent que l' enumerateObjects...énumération peut être plus lente que rapide avec une boucle. J'ai effectué ce test plusieurs milliers de fois; le corps du bloc et de la boucle était la même ligne de code: [(NSOperation *)obj cancel];. Les moyennes: boucle d'énumération rapide - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Fast enumeration time (for..in..loop): 0.000009et pour le bloc - -[JHStatusBar dequeueStatusMessage:] [Line: 147] Enumeration time using block: 0.000043. Bizarre que le décalage horaire soit si important et cohérent, mais, évidemment, il s'agit d'un cas de test très spécifique.
chown
42

Bien que cette question soit ancienne, les choses n'ont pas changé, la réponse acceptée est incorrecte.

L' enumerateObjectsUsingBlockAPI n'était pas censée remplacer for-in, mais pour un cas d'utilisation totalement différent:

  • Il permet l'application d'une logique arbitraire et non locale. c'est-à-dire que vous n'avez pas besoin de savoir ce que fait le bloc pour l'utiliser sur un tableau.
  • Énumération simultanée pour les grandes collections ou les calculs lourds (à l'aide du withOptions:paramètre)

L'énumération rapide avec for-inest toujours la méthode idiomatique d'énumération d'une collection.

Fast Enumeration bénéficie de la brièveté du code, de la lisibilité et des optimisations supplémentaires qui le rendent anormalement rapide. Plus rapide qu'une ancienne boucle for C!

Un test rapide conclut qu'en 2014 sur iOS 7, il enumerateObjectsUsingBlockest systématiquement 700% plus lent que for-in (basé sur des itérations de 1 mm d'un tableau de 100 éléments).

La performance est-elle ici une réelle préoccupation pratique?

Certainement pas, à de rares exceptions près.

Le but est de démontrer qu'il y a peu d'avantages à utiliser enumerateObjectsUsingBlock:over for-insans une très bonne raison. Cela ne rend pas le code plus lisible ... ni plus rapide ... ni thread-safe. (une autre idée fausse courante).

Le choix se résume à une préférence personnelle. Pour moi, l'option idiomatique et lisible gagne. Dans ce cas, c'est l'énumération rapide utilisant for-in.

Référence:

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

Résultats:

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
la source
4
Je peux confirmer qu'exécuter le même test sur un MacBook Pro Retina 2014 enumerateObjectsUsingBlockest en fait 5 fois plus lent. Cela est apparemment dû à un pool de libération automatique enveloppant chaque invocation du bloc, ce qui ne se produit pas pour le for incas.
Pol
2
Je confirme qu'il enumerateObjectsUsingBlock:est toujours 4X plus lent sur le vrai iPhone 6 iOS9, en utilisant Xcode 7.x pour construire.
Cœur
1
Merci d'avoir confirmé! Je souhaite que cette réponse ne soit pas si enterrée ... certaines personnes aiment juste la enumeratesyntaxe parce qu'elle ressemble à FP et ne veulent pas écouter l'analyse des performances.
Adam Kaplan
1
À propos, ce n'est pas seulement dû au pool de libération automatique. Il y a beaucoup plus de pile qui pousse et qui saute avecenumerate:
Adam Kaplan
24

Pour répondre à la question sur les performances, j'ai fait quelques tests en utilisant mon projet de test de performance . Je voulais savoir laquelle des trois options pour envoyer un message à tous les objets d'un tableau est la plus rapide.

Les options étaient:

1) makeObjectsPerformSelector

[arr makeObjectsPerformSelector:@selector(_stubMethod)];

2) énumération rapide et envoi régulier de messages

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

3) enumerateObjectsUsingBlock et envoi régulier de messages

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

Il s'avère que makeObjectsPerformSelector était de loin le plus lent. Il a fallu deux fois plus de temps que le dénombrement rapide. Et enumerateObjectsUsingBlock était le plus rapide, il était environ 15 à 20% plus rapide qu'une itération rapide.

Donc, si vous êtes très préoccupé par les meilleures performances possibles, utilisez enumerateObjectsUsingBlock. Mais gardez à l'esprit que dans certains cas, le temps nécessaire pour énumérer une collection est réduit par le temps nécessaire pour exécuter le code que vous souhaitez que chaque objet exécute.

ApprendreCocos2D
la source
Pouvez-vous indiquer votre test spécifique? Vous semblez avoir donné une mauvaise réponse.
Cœur
3

Il est assez utile d'utiliser enumerateObjectsUsingBlock comme boucle externe lorsque vous souhaitez interrompre les boucles imbriquées.

par exemple

[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'alternative consiste à utiliser des instructions goto.

Gabe
la source
1
Ou vous pouvez simplement revenir de la méthode comme vous l'avez fait ici :-D
Adam Kaplan
1

Merci à @bbum et @Chuck pour avoir lancé des comparaisons complètes sur les performances. Heureux de savoir que c'est trivial. Il me semble que je suis allé avec:

  • for (... in ...)- par défaut goto. Plus intuitif pour moi, plus d'historique de programmation ici que n'importe quelle préférence réelle - réutilisation de plusieurs langages, moins de frappe pour la plupart des structures de données grâce à l'auto-complétion IDE: P.

  • enumerateObject...- lorsque l'accès à l'objet et à l'index est nécessaire. Et lors de l'accès à des structures non-tableau ou dictionnaire (préférence personnelle)

  • for (int i=idx; i<count; i++) - pour les tableaux, quand j'ai besoin de démarrer sur un index non nul

enneigé
la source