Iterable e Sequence di Kotlin hanno lo stesso aspetto. Perché sono necessari due tipi?


88

Entrambe queste interfacce definiscono un solo metodo

public operator fun iterator(): Iterator<T>

La documentazione dice che Sequencedovrebbe essere pigro. Ma non è Iterableanche pigro (a meno che non sia supportato da a Collection)?

Risposte:


138

La differenza fondamentale risiede nella semantica e nell'implementazione delle funzioni di estensione stdlib per Iterable<T>e Sequence<T>.

  • Infatti Sequence<T>, le funzioni di estensione si eseguono pigramente dove possibile, in modo simile alle operazioni intermedie di Java Streams . Ad esempio, ne Sequence<T>.map { ... }restituisce un altro Sequence<R>e non elabora effettivamente gli elementi fino a quando non viene chiamata un'operazione da terminale come toListo fold.

    Considera questo codice:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    Stampa:

    before sum 1 2
    

    Sequence<T>è inteso per un utilizzo pigro e un pipelining efficiente quando si desidera ridurre il più possibile il lavoro svolto nelle operazioni del terminale , come per Java Streams. Tuttavia, la pigrizia introduce un po 'di overhead, che è indesiderabile per trasformazioni semplici comuni di raccolte più piccole e le rende meno performanti.

    In generale, non esiste un buon modo per determinare quando è necessario, quindi in Kotlin la pigrizia stdlib viene resa esplicita ed estratta Sequence<T>nell'interfaccia per evitare di usarla su tutti Iterablei messaggi di default.

  • Perché Iterable<T>, al contrario, le funzioni di estensione con semantica delle operazioni intermedie funzionano con entusiasmo, elaborano immediatamente gli elementi e ne restituiscono un altro Iterable. Ad esempio, Iterable<T>.map { ... }restituisce a List<R>con i risultati della mappatura.

    Il codice equivalente per Iterable:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    Questo stampa:

    1 2 before sum
    

    Come detto sopra, Iterable<T>non è pigro per impostazione predefinita, e questa soluzione si mostra bene: nella maggior parte dei casi ha una buona località di riferimento sfruttando così la cache della CPU, la previsione, il prefetching ecc. In modo che anche la copia multipla di una raccolta funzioni ancora bene abbastanza e funziona meglio in casi semplici con piccole raccolte.

    Se è necessario un maggiore controllo sulla pipeline di valutazione, è prevista una conversione esplicita in una sequenza pigra con Iterable<T>.asSequence()funzione.


3
Probabilmente una grande sorpresa per Java(soprattutto Guava) i fan
Venkata Raju il

@VenkataRaju per le persone funzionali potrebbero essere sorpresi dall'alternativa di pigro per impostazione predefinita.
Jayson Minard

9
Lazy per impostazione predefinita è generalmente meno performante per le raccolte più piccole e utilizzate più comunemente. Una copia può essere più veloce di una valutazione pigra se si sfrutta la cache della CPU e così via. Quindi per casi d'uso comuni è meglio non essere pigri. E sfortunatamente i contratti comuni per funzioni come map, filtere altri non contengono informazioni sufficienti per decidere se non dal tipo di raccolta di origine, e poiché la maggior parte delle raccolte sono anche iterabili, questo non è un buon indicatore per "essere pigri" perché è comunemente OVUNQUE. pigro deve essere esplicito per essere al sicuro.
Jayson Minard

1
@naki Un esempio da un recente annuncio di Apache Spark, si preoccupano ovviamente di questo, vedere la sezione "Calcolo consapevole della cache" su databricks.com/blog/2015/04/28/… ... ma sono preoccupati per miliardi di le cose si ripetono quindi devono andare all'estremo.
Jayson Minard,

3
Inoltre, una trappola comune con la valutazione pigra è catturare il contesto e archiviare il calcolo pigro risultante in un campo insieme a tutti i locali catturati e qualunque cosa in loro possesso. Quindi, difficile eseguire il debug delle perdite di memoria.
Ilya Ryzhenkov

50

Completamento della risposta del tasto di scelta rapida:

È importante notare come Sequence e Iterable itera attraverso i tuoi elementi:

Esempio di sequenza:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

Risultato registro:

filtro - Mappa - Ciascuno; filtro - Mappa - Ciascuno

Esempio ripetibile:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

filtro - filtro - Mappa - Mappa - Ciascuno - Ciascuno


5
Questo è un ottimo esempio della differenza tra i due.
Alexey Soshin

Questo è un ottimo esempio.
frye3k

2

Iterableè mappato java.lang.Iterableall'interfaccia in JVMed è implementato da raccolte di uso comune, come List o Set. Le funzioni di estensione della raccolta su queste vengono valutate con entusiasmo, il che significa che tutte elaborano immediatamente tutti gli elementi nel loro input e restituiscono una nuova raccolta contenente il risultato.

Ecco un semplice esempio di utilizzo delle funzioni di raccolta per ottenere i nomi delle prime cinque persone in un elenco la cui età è di almeno 21 anni:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

Piattaforma di destinazione: JVM in esecuzione su kotlin v. 1.3.61 In primo luogo, il controllo dell'età viene eseguito per ogni singola persona nell'elenco, con il risultato inserito in un elenco nuovo di zecca. Quindi, la mappatura ai loro nomi viene eseguita per ogni Persona che è rimasta dopo l'operatore di filtro, finendo in un altro nuovo elenco (questo ora è un List<String>). Infine, c'è un ultimo nuovo elenco creato per contenere i primi cinque elementi dell'elenco precedente.

Al contrario, Sequence è un nuovo concetto in Kotlin per rappresentare una raccolta di valori valutata pigramente. Le stesse estensioni di raccolta sono disponibili per l' Sequenceinterfaccia, ma restituiscono immediatamente istanze di Sequence che rappresentano uno stato elaborato della data, ma senza elaborare effettivamente alcun elemento. Per iniziare l'elaborazione, la Sequencedeve essere terminata con un operatore terminale, questi sono fondamentalmente una richiesta alla Sequenza di materializzare i dati che rappresenta in qualche forma concreta. Gli esempi includono toList, toSete sum, per citarne solo alcuni. Quando questi vengono chiamati, verrà elaborato solo il numero minimo richiesto di elementi per produrre il risultato richiesto.

Trasformare una raccolta esistente in una sequenza è piuttosto semplice, devi solo usare l' asSequenceestensione. Come accennato in precedenza, è necessario aggiungere anche un operatore di terminale, altrimenti la sequenza non eseguirà mai alcuna elaborazione (di nuovo, pigro!).

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

Piattaforma di destinazione: JVMRunning su kotlin v. 1.3.61 In questo caso, le istanze di Person nella sequenza vengono controllate per la loro età, se passano, il loro nome viene estratto e quindi aggiunto all'elenco dei risultati. Questo viene ripetuto per ogni persona nell'elenco originale finché non vengono trovate cinque persone. A questo punto, la funzione toList restituisce un elenco e il resto delle persone in Sequencenon viene elaborato.

C'è anche qualcosa in più di cui una sequenza è capace: può contenere un numero infinito di elementi. Con questa prospettiva, ha senso che gli operatori lavorino come fanno: un operatore su una sequenza infinita non potrebbe mai tornare se ha svolto il suo lavoro con entusiasmo.

Ad esempio, ecco una sequenza che genererà tante potenze di 2 quante sono richieste dal suo operatore di terminale (ignorando il fatto che questo traboccherebbe rapidamente):

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

Puoi trovare di più qui .

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.