Peut-il y avoir une raison de préférer filter+map
:
list.filter (i => aCondition(i)).map(i => fun(i))
sur collect
? :
list.collect(case i if aCondition(i) => fun(i))
Celui avec collect
(look unique) me semble plus rapide et plus propre. Donc, je choisirais toujours collect
.
La plupart des collections de Scala appliquent avec empressement des opérations et (sauf si vous utilisez une bibliothèque de macros qui le fait pour vous) ne fusionnera pas les opérations. Ainsi, filter
suivi de map
créera généralement deux collections (et même si vous utilisez Iterator
ou une telle méthode, la forme intermédiaire sera créée de manière transitoire, bien qu’un élément à la fois), alors que collect
ne le sera pas.
Par contre, collect
utilise une fonction partielle pour implémenter le test conjoint, et les fonctions partielles sont plus lentes que les prédicats (A => Boolean
) pour vérifier si quelque chose se trouve dans la collection.
En outre, dans certains cas, il est tout simplement plus simple de lire l'un que l'autre sans se soucier des performances ou des différences d'utilisation de la mémoire d'un facteur 2 ou plus. Dans ce cas, utilisez ce qui est le plus clair. Généralement, si vous avez déjà nommé les fonctions, il est plus clair de lire
xs.filter(p).map(f)
xs.collect{ case x if p(x) => f(x) }
mais si vous fournissez les fermetures en ligne, collect
paraît généralement plus propre
xs.filter(x < foo(x, x)).map(x => bar(x, x))
xs.collect{ case x if foo(x, x) => bar(x, x) }
même si ce n'est pas nécessairement plus court, car vous ne faites référence à la variable qu'une seule fois.
Maintenant, quelle est la différence de performance? Cela varie, mais si nous considérons une collection comme celle-ci:
val v = Vector.tabulate(10000)(i => ((i%100).toString, (i%7).toString))
et vous voulez choisir la deuxième entrée en fonction du filtrage de la première (pour que les opérations de filtrage et de cartographie soient vraiment faciles), nous obtenons le tableau suivant.
Remarque: vous pouvez obtenir des vues paresseuses dans des collections et y regrouper des opérations. Vous ne récupérez pas toujours votre type d'origine, mais vous pouvez toujours utiliser to
pour obtenir le bon type de collection. Donc, xs.view.filter(p).map(f).toVector
, à cause de la vue, ne créerait pas d'intermédiaire. Cela est testé ci-dessous aussi. Il a également été suggéré que l'on peut xs.flatMap(x => if (p(x)) Some(f(x)) else None)
et que ceci est efficient. Ce n'est pas le cas. Il est également testé ci-dessous. Et vous pouvez éviter la fonction partielle en créant explicitement un générateur: val vb = Vector.newBuilder[String]; xs.foreach(x => if (p(x)) vb += f(x)); vb.result
, et les résultats correspondants sont également répertoriés ci-dessous.
Dans le tableau ci-dessous, trois conditions ont été testées: ne rien filtrer, ne filtrer que la moitié, tout filtrer. Les temps ont été normalisés pour filtrer/mapper (100% = même temps que le filtre/mapper, plus bas est meilleur). Les limites d'erreur sont autour de + - 3%.
Performances de différentes alternatives de filtre/carte
====================== Vector ========================
filter/map collect view filt/map flatMap builder
100% 44% 64% 440% 30% filter out none
100% 60% 76% 605% 42% filter out half
100% 112% 103% 1300% 74% filter out all
Ainsi, filter/map
et collect
sont généralement assez proches (avec collect
gagnant lorsque vous en gardez beaucoup), flatMap
est beaucoup plus lent dans toutes les situations et la création d'un générateur gagne toujours. (Ceci est vrai spécifiquement pour Vector
. D'autres collections peuvent avoir des caractéristiques quelque peu différentes, mais les tendances pour la plupart seront similaires car les différences dans les opérations sont similaires.) Les vues dans le test ont tendance à être gagnantes. , mais ils ne fonctionnent pas toujours de manière transparente (et ils ne sont pas vraiment meilleurs que collect
sauf pour le cas vide).
Donc, en bout de ligne: préférez filter
puis map
si cela facilite la clarté lorsque la vitesse importe peu, ou préférez-le pour la vitesse lorsque vous filtrez presque tout, mais que vous souhaitez tout de même garder les éléments fonctionnels (évitez donc d'utiliser un générateur ) et sinon, utilisez collect
.
Je suppose que ceci est plutôt basé sur l'opinion, mais étant donné les définitions suivantes:
scala> val l = List(1,2,3,4)
l: List[Int] = List(1, 2, 3, 4)
scala> def f(x: Int) = 2*x
f: (x: Int)Int
scala> def p(x: Int) = x%2 == 0
p: (x: Int)Boolean
Lequel des deux trouvez-vous plus agréable à lire:
l.filter(p).map(f)
ou
l.collect{ case i if p(i) => f(i) }
(Notez que je devais corriger votre syntaxe ci-dessus, car vous avez besoin du crochet et de case
pour ajouter la condition if
).
Personnellement, je trouve la filter
+ map
beaucoup plus agréable à lire et à comprendre. Tout dépend de la syntaxe exacte que vous utilisez, mais étant donné p
et f
, vous n'avez pas à écrire de fonctions anonymes lorsque vous utilisez filter
ou map
, vous en avez besoin lorsque vous utilisez collect.
Vous avez également la possibilité d'utiliser flatMap
:
l.flatMap(i => if(p(i)) Some(f(i)) else None)
Laquelle est probablement la plus efficace parmi les 3 solutions, mais je la trouve moins agréable que map
et filter
.
Globalement, il est très difficile de dire laquelle serait la plus rapide, car cela dépend en grande partie des optimisations qui seront finalement exécutées par scalac
, puis par la JVM. Tous les 3 devraient être assez proches, et certainement pas un facteur pour décider lequel utiliser.
Un cas où filter/map
est plus propre, c'est quand vous voulez aplatir le résultat du filtre.
def getList(x: Int) = {
List.range(x, 0, -1)
}
val xs = List(1,2,3,4)
//Using filter and flatMap
xs.filter(_ % 2 == 0).flatMap(getList)
//Using collect and flatten
xs.collect{ case x if x % 2 == 0 => getList(x)}.flatten