Kotlins Iterable und Sequence sehen genau gleich aus. Warum sind zwei Typen erforderlich?

85

Beide Schnittstellen definieren nur eine Methode

public operator fun iterator(): Iterator<T>

Die Dokumentation sagt Sequence, es soll faul sein. Aber ist das nicht auch Iterablefaul (es sei denn, es wird von a unterstützt Collection)?

Venkata Raju
quelle

Antworten:

135

Der Hauptunterschied liegt in der Semantik und der Implementierung der stdlib-Erweiterungsfunktionen für Iterable<T>und Sequence<T>.

  • Denn Sequence<T>die Erweiterungsfunktionen arbeiten nach Möglichkeit träge, ähnlich wie Java Streams- Zwischenoperationen . Gibt beispielsweise eine Sequence<T>.map { ... }andere zurück Sequence<R>und verarbeitet die Elemente erst dann, wenn eine Terminaloperation wie toListoder foldaufgerufen wird.

    Betrachten Sie diesen Code:

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

    Es druckt:

    before sum 1 2
    

    Sequence<T>ist für eine verzögerte Verwendung und ein effizientes Pipelining gedacht, wenn Sie die Arbeit im Terminalbetrieb so weit wie möglich reduzieren möchten, genau wie bei Java Streams. Faulheit führt jedoch zu einem gewissen Overhead, der für übliche einfache Transformationen kleinerer Sammlungen unerwünscht ist und sie weniger leistungsfähig macht.

    Im Allgemeinen gibt es keine gute Möglichkeit, um festzustellen, wann es benötigt wird. Daher wird in Kotlin stdlib faul gemacht und in die Sequence<T>Schnittstelle extrahiert, um zu vermeiden, dass es Iterablestandardmäßig auf allen s verwendet wird.

  • Im Iterable<T>Gegensatz dazu arbeiten die Erweiterungsfunktionen mit Zwischenoperationssemantik eifrig, verarbeiten die Elemente sofort und geben eine andere zurück Iterable. Gibt beispielsweise Iterable<T>.map { ... }a List<R>mit den darin enthaltenen Mapping-Ergebnissen zurück.

    Der entsprechende Code für Iterable:

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

    Dies druckt aus:

    1 2 before sum
    

    Wie oben erwähnt, Iterable<T>ist es standardmäßig nicht faul, und diese Lösung zeigt sich gut: In den meisten Fällen weist sie eine gute Referenzlokalität auf, wodurch der CPU-Cache, die Vorhersage, das Vorabrufen usw. genutzt werden, sodass auch das mehrfache Kopieren einer Sammlung noch gut funktioniert genug und ist in einfachen Fällen mit kleinen Sammlungen besser.

    Wenn Sie mehr Kontrolle über die Auswertungspipeline benötigen, erfolgt eine explizite Konvertierung in eine verzögerte Sequenz mit Iterable<T>.asSequence()Funktion.

Hotkey
quelle
3
Wahrscheinlich eine große Überraschung für Java(meistens Guava) Fans
Venkata Raju
@ VenkataRaju für funktionierende Leute könnten sie über die Alternative von faul standardmäßig überrascht sein.
Jayson Minard
9
Lazy ist normalerweise für kleinere und häufiger verwendete Sammlungen weniger leistungsfähig. Eine Kopie kann schneller sein als eine verzögerte Auswertung, wenn der CPU-Cache usw. genutzt wird. Für häufige Anwendungsfälle ist es also besser, nicht faul zu sein. Und leider sind die gemeinsamen Verträge für Funktionen wie map, filterund andere tragen nicht genügend Informationen , andere zu entscheiden , als von der Quelle Kollektionstyp, und da die meisten Sammlungen auch Iterable sind, das ist kein guter Marker für „faul“ , weil es häufig überall. faul muss explizit sein, um sicher zu sein.
Jayson Minard
1
@naki Ein Beispiel aus einer kürzlich angekündigten Apache Spark-Ankündigung, über die sie sich offensichtlich Sorgen machen, siehe Abschnitt "Cache- fähige Berechnung" unter databricks.com/blog/2015/04/28/… ... aber sie sind besorgt über Milliarden von Dinge iterieren, so dass sie bis zum Äußersten gehen müssen.
Jayson Minard
3
Darüber hinaus besteht eine häufige Gefahr bei der verzögerten Auswertung darin, den Kontext zu erfassen und die daraus resultierenden verzögerten Berechnungen zusammen mit allen erfassten Einheimischen und allem, was sie enthalten, in einem Feld zu speichern. Daher ist es schwierig, Speicherlecks zu debuggen.
Ilya Ryzhenkov
49

Antwort des Hotkeys vervollständigen:

Es ist wichtig zu beachten, wie Sequence und Iterable in Ihren Elementen iteriert:

Sequenzbeispiel:

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

Protokollergebnis:

Filter - Karte - Jeder; Filter - Karte - Jeder

Iterierbares Beispiel:

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

Filter - Filter - Karte - Karte - Jeder - Jeder

Leandro Borges Ferreira
quelle
5
Das ist ein hervorragendes Beispiel für den Unterschied zwischen den beiden.
Alexey Soshin
Dies ist ein großartiges Beispiel.
frye3k
2

Iterablewird der java.lang.IterableSchnittstelle auf dem zugeordnet JVMund von häufig verwendeten Sammlungen wie List oder Set implementiert. Die Sammlungserweiterungsfunktionen auf diesen werden eifrig ausgewertet, was bedeutet, dass sie alle Elemente in ihrer Eingabe sofort verarbeiten und eine neue Sammlung zurückgeben, die das Ergebnis enthält.

Hier ist ein einfaches Beispiel für die Verwendung der Erfassungsfunktionen, um die Namen der ersten fünf Personen in einer Liste abzurufen, deren Alter mindestens 21 Jahre beträgt:

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

Zielplattform: JVMRunning auf kotlin v. 1.3.61 Zunächst wird die Altersprüfung für jede einzelne Person in der Liste durchgeführt, wobei das Ergebnis in eine brandneue Liste aufgenommen wird. Dann wird die Zuordnung zu ihren Namen für jede Person durchgeführt, die nach dem Filteroperator verblieben ist und in einer weiteren neuen Liste endet (dies ist jetzt eine List<String>). Schließlich wird eine letzte neue Liste erstellt, die die ersten fünf Elemente der vorherigen Liste enthält.

Im Gegensatz dazu ist Sequence ein neues Konzept in Kotlin, um eine träge bewertete Sammlung von Werten darzustellen. Die gleichen Sammlungserweiterungen sind für die SequenceSchnittstelle verfügbar , diese geben jedoch sofort Sequenzinstanzen zurück, die einen verarbeiteten Status des Datums darstellen, ohne jedoch tatsächlich Elemente zu verarbeiten. Um die Verarbeitung zu starten, Sequencemuss das mit einem Terminalbetreiber beendet werden. Dies ist im Grunde eine Aufforderung an die Sequenz, die Daten, die sie darstellt, in einer konkreten Form zu materialisieren. Beispiele sind toList, toSetund sum, um nur einige zu nennen. Wenn diese aufgerufen werden, wird nur die minimal erforderliche Anzahl von Elementen verarbeitet, um das angeforderte Ergebnis zu erzielen.

Das Transformieren einer vorhandenen Sammlung in eine Sequenz ist ziemlich einfach. Sie müssen nur die asSequenceErweiterung verwenden. Wie oben erwähnt, müssen Sie auch einen Terminaloperator hinzufügen, da die Sequenz sonst niemals verarbeitet wird (wieder faul!).

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

Zielplattform: JVMRunning auf kotlin v. 1.3.61 In diesem Fall werden die Personeninstanzen in der Sequenz jeweils auf ihr Alter überprüft. Wenn sie bestanden werden, wird ihr Name extrahiert und dann zur Ergebnisliste hinzugefügt. Dies wird für jede Person in der ursprünglichen Liste wiederholt, bis fünf Personen gefunden wurden. Zu diesem Zeitpunkt gibt die Funktion toList eine Liste zurück, und der Rest der Personen in der Sequencewird nicht verarbeitet.

Es gibt noch etwas Besonderes, zu dem eine Sequenz in der Lage ist: Sie kann unendlich viele Elemente enthalten. Vor diesem Hintergrund ist es sinnvoll, dass Operatoren so arbeiten, wie sie es tun - ein Operator mit einer unendlichen Sequenz könnte niemals zurückkehren, wenn er seine Arbeit eifrig erledigt hätte.

Als Beispiel ist hier eine Sequenz, die so viele Zweierpotenzen erzeugt, wie von ihrem Terminalbetreiber benötigt wird (wobei die Tatsache ignoriert wird, dass dies schnell überlaufen würde):

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

Mehr finden Sie hier .

Sazzad Hissain Khan
quelle