val-veränderlich versus var-unveränderlich in Scala

97

Gibt es in Scala Richtlinien, wann val mit einer veränderlichen Sammlung verwendet werden soll, während var mit einer unveränderlichen Sammlung verwendet werden soll? Oder sollten Sie wirklich Wert auf eine unveränderliche Sammlung legen?

Die Tatsache, dass es beide Arten von Sammlungen gibt, gibt mir eine große Auswahl, und oft weiß ich nicht, wie ich diese Auswahl treffen soll.

James McCabe
quelle
Siehe zum Beispiel stackoverflow.com/questions/10999024/…
Luigi Plinge

Antworten:

104

Ziemlich häufige Frage, diese. Das Schwierige ist, die Duplikate zu finden.

Sie sollten sich um referenzielle Transparenz bemühen . Was das bedeutet , ist , dass, wenn ich einen Ausdruck „e“ haben, könnte ich eine machen val x = e, und ersetzen emit x. Dies ist die Eigenschaft, die die Veränderlichkeit beeinträchtigt. Wenn Sie eine Entwurfsentscheidung treffen müssen, maximieren Sie die referenzielle Transparenz.

In der Praxis ist eine lokale Methode vardie sicherste var, die es gibt, da sie der Methode nicht entgeht. Wenn die Methode kurz ist, noch besser. Wenn dies nicht der Fall ist, versuchen Sie, es durch Extrahieren anderer Methoden zu reduzieren.

Auf der anderen Seite hat ein veränderliches Sammlung das Potenzial zu entkommen, auch wenn dies nicht der Fall. Wenn Sie Code ändern, möchten Sie ihn möglicherweise an andere Methoden übergeben oder zurückgeben. So etwas bricht die referenzielle Transparenz.

Auf einem Objekt (einem Feld) passiert so ziemlich dasselbe, aber mit schlimmeren Konsequenzen. In beiden Fällen hat das Objekt den Status und unterbricht daher die referenzielle Transparenz. Eine veränderbare Sammlung bedeutet jedoch, dass sogar das Objekt selbst die Kontrolle darüber verlieren kann, wer es ändert.

Daniel C. Sobral
quelle
38
Schönes, neues Gesamtbild in meinem Kopf: Lieber immutable valüber immutable varals mutable valüber mutable var. Besonders immutable varvorbei mutable val!
Peter Schmitz
2
Denken Sie daran, dass Sie immer noch (wie beim Durchsickern einer nebenwirkenden "Funktion", die sie ändern kann) über eine lokale veränderbare Funktion schließen können var. Ein weiteres nettes Merkmal der Verwendung unveränderlicher Sammlungen ist, dass Sie alte Kopien effizient aufbewahren können, selbst wenn die varmutiert sind.
Geheimnisvoller Dan
1
tl; dr: lieber var x: Set[Int] als val x: mutable.Set[Int] wenn Sie xzu einer anderen Funktion übergehen , im ersteren Fall sind Sie sicher, dass diese Funktion xfür Sie nicht mutieren kann.
Pathikrit
17

Wenn Sie mit unveränderlichen Sammlungen arbeiten und diese "ändern" müssen, z. B. Elemente in einer Schleife hinzufügen, müssen Sie vars verwenden, da Sie die resultierende Sammlung irgendwo speichern müssen. Wenn Sie nur aus unveränderlichen Sammlungen lesen, verwenden Sie vals.

Stellen Sie im Allgemeinen sicher, dass Sie Referenzen und Objekte nicht verwechseln. vals sind unveränderliche Referenzen (konstante Zeiger in C). Das heißt, wenn Sie verwenden val x = new MutableFoo(), werden Sie in der Lage , das Objekt zu ändern , dass xPunkte, aber Sie werden nicht in der Lage zu ändern , auf die xObjektpunkte. Das Gegenteil gilt, wenn Sie verwenden var x = new ImmutableFoo(). Ich nehme meinen ersten Rat auf: Wenn Sie nicht ändern müssen, auf welches Objekt ein Referenzpunkt zeigt, verwenden Sie vals.

Malte Schwerhoff
quelle
1
var immutable = something(); immutable = immutable.update(x)vereitelt den Zweck der Verwendung einer unveränderlichen Sammlung. Sie haben die referenzielle Transparenz bereits aufgegeben und können normalerweise den gleichen Effekt aus einer veränderlichen Sammlung mit besserer Zeitkomplexität erzielen. Von den vier Möglichkeiten ( valund varveränderlich und unveränderlich) ist diese am wenigsten sinnvoll. Ich benutze oft val mutable.
Jim Pivarski
3
@ JimPivarski Ich bin anderer Meinung als andere, siehe Daniels Antwort und den Kommentar von Peter. Wenn Sie eine Datenstruktur aktualisieren müssen, hat die Verwendung einer unveränderlichen Variable anstelle eines veränderlichen Werts den Vorteil, dass Sie Verweise auf die Struktur verlieren können, ohne das Risiko einzugehen, dass sie von anderen so geändert wird, dass Ihre lokalen Annahmen verletzt werden. Der Nachteil für diese "anderen" ist, dass sie möglicherweise veraltete Daten lesen.
Malte Schwerhoff
Ich habe meine Meinung geändert und stimme Ihnen zu (ich hinterlasse meinen ursprünglichen Kommentar für die Geschichte). Ich habe das seitdem benutzt, besonders in var list: List[X] = Nil; list = item :: list; ...und ich hatte vergessen, dass ich einmal anders geschrieben habe.
Jim Pivarski
@MalteSchwerhoff: "veraltete Daten" sind tatsächlich wünschenswert, je nachdem, wie Sie Ihr Programm entworfen haben, wenn Konsistenz entscheidend ist; Dies ist beispielsweise eines der wichtigsten Prinzipien für die Funktionsweise der Parallelität in Clojure.
Erik Kaplun
@ErikAllik Ich würde nicht sagen, dass veraltete Daten per se wünschenswert sind, aber ich stimme darin überein, dass sie vollkommen in Ordnung sein können, abhängig von den Garantien, die Sie Ihren Kunden geben möchten / müssen. Oder haben Sie ein Beispiel, bei dem das alleinige Lesen veralteter Daten tatsächlich von Vorteil ist? Ich meine nicht die Konsequenzen des Akzeptierens veralteter Daten, die eine bessere Leistung oder eine einfachere API sein könnten.
Malte Schwerhoff
9

Der beste Weg, dies zu beantworten, ist mit einem Beispiel. Nehmen wir an, wir haben einen Prozess, bei dem aus irgendeinem Grund einfach Zahlen gesammelt werden. Wir möchten diese Nummern protokollieren und senden die Sammlung dazu an einen anderen Prozess.

Natürlich sammeln wir immer noch Zahlen, nachdem wir die Sammlung an den Logger gesendet haben. Angenommen, der Protokollierungsprozess verursacht einen gewissen Aufwand, der die eigentliche Protokollierung verzögert. Hoffentlich können Sie sehen, wohin das führt.

Wenn wir diese Sammlung in einer veränderlichen Sammlung speichern val(veränderbar, weil wir sie kontinuierlich erweitern), bedeutet dies, dass der Prozess, der die Protokollierung durchführt, dasselbe Objekt betrachtet , das von unserem Erfassungsprozess noch aktualisiert wird. Diese Sammlung kann jederzeit aktualisiert werden. Wenn also Zeit für die Protokollierung ist, protokollieren wir die von uns gesendete Sammlung möglicherweise nicht.

Wenn wir vareine unveränderliche Datenstruktur verwenden , senden wir eine unveränderliche Datenstruktur an den Logger. Wenn wir unserer Sammlung weitere Zahlen hinzufügen, werden wir unsere durch eine neue unveränderliche Datenstruktur ersetzen . Dies bedeutet nicht, dass die an den Logger gesendete Sammlung ersetzt wird! Es verweist immer noch auf die Sammlung, die gesendet wurde. Unser Logger protokolliert also tatsächlich die empfangene Sammlung.var

jmazin
quelle
1

Ich denke, die Beispiele in diesem Blog-Beitrag werden mehr Licht ins Dunkel bringen, da die Frage, welche Kombination verwendet werden soll, in Parallelitätsszenarien noch wichtiger wird: Bedeutung der Unveränderlichkeit für die Parallelität . Und wenn wir schon dabei sind, beachten Sie die bevorzugte Verwendung von synchronisiertem vs @volatile vs etwas wie AtomicReference: drei Tools

Bogdan Nicolau
quelle
-2

var immutable vs. val mutable

Neben vielen hervorragenden Antworten auf diese Frage. Hier ist ein einfaches Beispiel, das mögliche Gefahren von Folgendem veranschaulicht val mutable:

Veränderbare Objekte können innerhalb von Methoden geändert werden, die sie als Parameter verwenden, während eine Neuzuweisung nicht zulässig ist.

import scala.collection.mutable.ArrayBuffer

object MyObject {
    def main(args: Array[String]) {

        val a = ArrayBuffer(1,2,3,4)
        silly(a)
        println(a) // a has been modified here
    }

    def silly(a: ArrayBuffer[Int]): Unit = {
        a += 10
        println(s"length: ${a.length}")
    }
}

Ergebnis:

length: 5
ArrayBuffer(1, 2, 3, 4, 10)

So etwas kann nicht passieren var immutable, da eine Neuzuweisung nicht zulässig ist:

object MyObject {
    def main(args: Array[String]) {
        var v = Vector(1,2,3,4)
        silly(v)
        println(v)
    }

    def silly(v: Vector[Int]): Unit = {
        v = v :+ 10 // This line is not valid
        println(s"length of v: ${v.length}")
    }
}

Ergebnisse in:

error: reassignment to val

Da Funktionsparameter so behandelt werden, ist valdiese Neuzuweisung nicht zulässig.

Akavall
quelle
Das ist falsch. Der Grund, warum Sie diesen Fehler erhalten haben, ist, dass Sie in Ihrem zweiten Beispiel Vector verwendet haben, das standardmäßig unveränderlich ist. Wenn Sie einen ArrayBuffer verwenden, wird dieser gut kompiliert und macht dasselbe, indem er nur das neue Element hinzufügt und den mutierten Puffer druckt. pastebin.com/vfq7ytaD
EdgeCaseBerg
@EdgeCaseBerg, ich verwende in meinem zweiten Beispiel absichtlich einen Vektor, weil ich zu zeigen versuche, dass das Verhalten des ersten Beispiels mutable valmit dem nicht möglich ist immutable var. Was ist hier falsch?
Akavall
Sie vergleichen hier Äpfel mit Orangen. Vektor hat keine +=Methode wie Array-Puffer. Ihre Antworten implizieren, dass dies +=dasselbe ist, x = x + ywas es nicht ist. Ihre Aussage, dass Funktionsparameter als vals behandelt werden, ist korrekt und Sie erhalten den von Ihnen erwähnten Fehler, jedoch nur, weil Sie ihn verwendet haben =. Sie können denselben Fehler mit einem ArrayBuffer erhalten, sodass die Veränderbarkeit der Sammlungen hier nicht wirklich relevant ist. Es ist also keine gute Antwort, weil es nicht zu dem kommt, worüber das OP spricht. Obwohl es ein gutes Beispiel für die Gefahren ist, eine veränderliche Sammlung weiterzugeben, wenn Sie dies nicht beabsichtigt haben.
EdgeCaseBerg
@EdgeCaseBerg Aber Sie können das Verhalten, mit dem ich komme ArrayBuffer, nicht replizieren , indem Sie verwenden Vector. Die Frage des OP ist weit gefasst, aber sie suchten nach Vorschlägen, wann sie verwendet werden sollten. Daher halte ich meine Antwort für nützlich, da sie die Gefahren der Weitergabe veränderlicher Sammlungen aufzeigt (die Tatsache, dass valdies nicht hilft). immutable varist sicherer als mutable val.
Akavall