La maggior parte delle collezioni di Scala applica con impazienza le operazioni e (a meno che non si utilizzi una libreria di macro che fa ciò per te) non fonderà le operazioni. Quindi filter
seguito da map
di solito creeremo due raccolte (e anche se si utilizza Iterator
o somesuch, il modulo intermedio verrà creato transitoriamente, anche se solo un elemento alla volta), mentre lo collect
non lo farà.
D'altra parte, collect
utilizza una funzione parziale per implementare il test congiunto e le funzioni parziali sono più lente dei predicati (A => Boolean
) per verificare se qualcosa è presente nella raccolta.
Inoltre, ci possono essere casi in cui è semplicemente più chiaro leggere l'uno rispetto all'altro e non si preoccupano delle prestazioni o delle differenze di utilizzo della memoria di un fattore 2 o giù di lì. In tal caso, utilizzare quello che è più chiaro.Generalmente se avete già le funzioni di nome, è più chiaro da leggere
xs.filter(p).map(f)
xs.collect{ case x if p(x) => f(x) }
ma se si forniscono le chiusure in linea, collect
generalmente appare più pulito
xs.filter(x < foo(x, x)).map(x => bar(x, x))
xs.collect{ case x if foo(x, x) => bar(x, x) }
anche se non è necessariamente più breve, perché solo fare riferimento alla variabile una volta.
Ora, quanto è grande la differenza di prestazioni? Che varia, ma se consideriamo aa raccolta in questo modo:
val v = Vector.tabulate(10000)(i => ((i%100).toString, (i%7).toString))
e si vuole scegliere la seconda voce in base a filtrare il primo (in modo che le operazioni di filtro e mappa sono entrambi molto facile), allora si ottiene la seguente tabella.
Nota: è possibile ottenere visualizzazioni pigre in raccolte e raccogliere le operazioni lì. Non si ottiene sempre il tipo originale, ma è sempre possibile utilizzare to
per ottenere il giusto tipo di raccolta. Quindi, xs.view.filter(p).map(f).toVector
, a causa della vista, non creerebbe un intermedio. Anche questo è testato di seguito. È stato anche suggerito che si può xs.flatMap(x => if (p(x)) Some(f(x)) else None)
e che questo è efficiente. Non è così. È anche testato di seguito. E si può evitare la funzione parziale creando esplicitamente un costruttore: val vb = Vector.newBuilder[String]; xs.foreach(x => if (p(x)) vb += f(x)); vb.result
, ei risultati per quello sono anche elencati di seguito.
Nella tabella seguente, sono state testate tre condizioni: non filtrare nulla, filtrare la metà, filtrare tutto. I tempi sono stati normalizzati per filtrare/mappare (100% = stesso tempo di filtro/mappa, inferiore è meglio). I limiti di errore si aggirano intorno al + - 3%.
prestazioni di diverse alternative di filtro/mappa
====================== 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
Così, filter/map
e collect
sono generalmente abbastanza vicino (con collect
vincere quando si tiene un sacco), flatMap
è di gran lunga inferiore in tutte le situazioni, e la creazione di un costruttore vince sempre. (Questo è vero in particolare per Vector
. Altre collezioni possono avere caratteristiche leggermente diverse, ma le tendenze per la maggior parte saranno simili perché le differenze nelle operazioni sono simili.) Visualizzazioni in questo test tendono ad essere una vittoria, ma non funzionano sempre senza problemi (e non sono realmente migliori di collect
tranne per il caso vuoto).
Quindi, linea di fondo: preferire filter
quindi map
se aiuta la chiarezza quando la velocità non ha importanza, o preferisci la velocità quando stai filtrando quasi tutto ma vuoi comunque mantenere le cose funzionanti (quindi non voglio usare un costruttore); e in caso contrario utilizzare collect
.
Non sono sicuro che le prestazioni sarebbero molto diverse. Entrambi attraversano l'intera sequenza una sola volta e applicano la funzione di predicato solo agli elementi che soddisfano la condizione, quindi entrambi stanno facendo la stessa quantità di lavoro base. Dipende davvero da quanto è intelligente il compilatore, quanto è grande la sequenza e quanto costano la condizione e la funzione da eseguire. –
Anche se suppongo che il secondo possa allocare una sequenza in meno, dal momento che non ha bisogno di problemi con il ritorno dalla chiamata 'filter'. –
@IanMcLaird Penso che questo sia il dibattito principale. È vero che il secondo assegna una sequenza in meno (e quindi più efficiente)? – Daniel