val-mutable versus var-immutable dans Scala

99

Existe-t-il des directives dans Scala sur le moment d'utiliser val avec une collection mutable par rapport à l'utilisation de var avec une collection immuable? Ou devriez-vous vraiment viser le val avec une collection immuable?

Le fait qu'il existe les deux types de collections me donne beaucoup de choix, et souvent je ne sais pas comment faire ce choix.

James McCabe
la source
Voir par exemple stackoverflow.com/questions/10999024/…
Luigi Plinge

Réponses:

105

Question assez courante, celle-ci. Le plus difficile est de trouver les doublons.

Vous devez viser la transparence référentielle . Cela signifie que si j'ai une expression «e», je pourrais créer un val x = eet le remplacer epar x. C'est la propriété qui rompt la mutabilité. Chaque fois que vous avez besoin de prendre une décision de conception, maximisez la transparence référentielle.

En pratique, une méthode locale varest la plus sûre varqui existe, car elle n'échappe pas à la méthode. Si la méthode est courte, c'est encore mieux. Si ce n'est pas le cas, essayez de le réduire en extrayant d'autres méthodes.

D'un autre côté, une collection mutable a le potentiel de s'échapper, même si ce n'est pas le cas. Lors du changement de code, vous pouvez alors vouloir le passer à d'autres méthodes ou le renvoyer. C'est le genre de chose qui brise la transparence référentielle.

Sur un objet (un champ), à peu près la même chose se produit, mais avec des conséquences plus désastreuses. Dans tous les cas, l'objet aura un état et, par conséquent, rompra la transparence référentielle. Mais avoir une collection mutable signifie que même l'objet lui-même peut perdre le contrôle de qui le change.

Daniel C. Sobral
la source
39
Nice, nouvelle grande image dans mon esprit: Je préfère immutable valplus immutable varsur mutable valplus mutable var. Surtout immutable varfini mutable val!
Peter Schmitz
2
Gardez à l'esprit que vous pouvez toujours fermer (comme dans la fuite une «fonction» à effet secondaire qui peut la changer) sur un mutable local var. Une autre fonctionnalité intéressante de l'utilisation de collections immuables est que vous pouvez conserver efficacement les anciennes copies, même si elles varmute.
Mysterious Dan
1
tl; dr: préférez var x: Set[Int] plus val x: mutable.Set[Int] car si vous passez xà une autre fonction, dans le premier cas, vous êtes sûr que cette fonction ne peut pas muter xpour vous.
pathikrit
17

Si vous travaillez avec des collections immuables et que vous avez besoin de les «modifier», par exemple, y ajouter des éléments dans une boucle, vous devez utiliser vars car vous devez stocker la collection résultante quelque part. Si vous ne lisez qu'à partir de collections immuables, utilisez vals.

En général, assurez-vous de ne pas confondre références et objets. vals sont des références immuables (pointeurs constants en C). Autrement dit, lorsque vous utilisez val x = new MutableFoo(), vous pourrez changer l'objet vers lequel xpointe, mais vous ne pourrez pas changer vers quel objet x pointe. Le contraire est vrai si vous utilisez var x = new ImmutableFoo(). Reprenant mon premier conseil: si vous n'avez pas besoin de changer vers quel objet une référence pointe, utilisez vals.

Malte Schwerhoff
la source
1
var immutable = something(); immutable = immutable.update(x)va à l'encontre de l'objectif d'utiliser une collection immuable. Vous avez déjà abandonné la transparence référentielle et vous pouvez généralement obtenir le même effet à partir d'une collection mutable avec une meilleure complexité temporelle. Des quatre possibilités ( valet var, mutables et immuables), celle-ci a le moins de sens. J'utilise souvent val mutable.
Jim Pivarski du
3
@JimPivarski Je ne suis pas d'accord, comme d'autres, voir la réponse de Daniel et le commentaire de Peter. Si vous avez besoin de mettre à jour une structure de données, utiliser un var immuable au lieu d'un val mutable a l'avantage que vous pouvez divulguer des références à la structure sans risquer qu'elle soit modifiée par d'autres d'une manière qui rompt vos hypothèses locales. L'inconvénient pour ces «autres» est qu'ils peuvent lire des données périmées.
Malte Schwerhoff
J'ai changé d'avis et je suis d'accord avec vous (je laisse mon commentaire original pour l'histoire). J'ai depuis utilisé ceci, en particulier dans var list: List[X] = Nil; list = item :: list; ...et j'avais oublié que j'avais écrit différemment une fois.
Jim Pivarski
@MalteSchwerhoff: des "données périmées" sont en fait souhaitables, selon la façon dont vous avez conçu votre programme, si la cohérence est cruciale; c'est par exemple l'un des principaux principes sous-jacents du fonctionnement de la concurrence dans Clojure.
Erik Kaplun
@ErikAllik Je ne dirais pas que les données périmées sont souhaitables en soi, mais je suis d'accord en ce sens que cela peut parfaitement convenir, en fonction des garanties que vous voulez / devez donner à vos clients. Ou avez-vous un exemple où le seul fait de lire des données périmées est en fait un avantage? Je ne parle pas des conséquences de l'acceptation de données périmées, qui pourraient être de meilleures performances ou une API plus simple.
Malte Schwerhoff
9

La meilleure façon de répondre est de donner un exemple. Supposons que nous ayons un processus qui collecte simplement des nombres pour une raison quelconque. Nous souhaitons enregistrer ces numéros et enverrons la collection à un autre processus pour ce faire.

Bien sûr, nous continuons à collecter des numéros après avoir envoyé la collection à l'enregistreur. Et disons qu'il y a une surcharge dans le processus de journalisation qui retarde la journalisation réelle. J'espère que vous pouvez voir où cela va.

Si nous stockons cette collection dans un mutable val, (mutable car nous y ajoutons continuellement), cela signifie que le processus effectuant la journalisation examinera le même objet qui est toujours mis à jour par notre processus de collecte. Cette collection peut être mise à jour à tout moment, et donc quand il est temps de se connecter, il se peut que nous n'enregistrions pas réellement la collection que nous avons envoyée.

Si nous utilisons un immuable var, nous envoyons une structure de données immuable à l'enregistreur. Lorsque nous ajouterons plus de numéros à notre collection, nous remplacerons notre varpar une nouvelle structure de données immuable . Cela ne signifie pas que la collecte envoyée à l'enregistreur est remplacée! Il fait toujours référence à la collection qui lui a été envoyée. Notre enregistreur enregistrera donc bien la collection qu'il a reçue.

jmazin
la source
1

Je pense que les exemples de cet article de blog éclaireront davantage, car la question de savoir quel combo utiliser devient encore plus importante dans les scénarios de concurrence: importance de l'immuabilité pour la concurrence . Et pendant que nous y sommes, notez l'utilisation préférée de synchronized vs @volatile vs quelque chose comme AtomicReference: trois outils

Bogdan Nicolau
la source
-2

var immutable contre. val mutable

En plus de nombreuses excellentes réponses à cette question. Voici un exemple simple, qui illustre les dangers potentiels de val mutable:

Les objets mutables peuvent être modifiés à l'intérieur de méthodes, qui les prennent comme paramètres, tandis que la réaffectation n'est pas autorisée.

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}")
    }
}

Résultat:

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

Une telle chose ne peut pas se produire avec var immutable, car la réaffectation n'est pas autorisée:

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}")
    }
}

Résulte en:

error: reassignment to val

Étant donné que les paramètres de fonction sont traités, valcette réaffectation n'est pas autorisée.

Akavall
la source
Ceci est une erreur. La raison pour laquelle vous avez cette erreur est que vous avez utilisé Vector dans votre deuxième exemple qui, par défaut, est immuable. Si vous utilisez un ArrayBuffer, vous verrez qu'il se compile correctement et fait la même chose où il ajoute simplement le nouvel élément et imprime le tampon muté. pastebin.com/vfq7ytaD
EdgeCaseBerg
@EdgeCaseBerg, j'utilise intentionnellement un vecteur dans mon deuxième exemple, car j'essaie de montrer que le comportement du premier exemple mutable valn'est pas possible avec le immutable var. Qu'est-ce qui est incorrect ici?
Akavall le
Vous comparez ici des pommes et des oranges. Vector n'a pas de +=méthode comme le tampon de tableau. Vos réponses impliquent que ce +=n'est pas la même x = x + ychose. Votre déclaration selon laquelle les paramètres de fonction sont traités comme des vals est correcte et vous obtenez l'erreur que vous mentionnez, mais uniquement parce que vous l'avez utilisée =. Vous pouvez obtenir la même erreur avec un ArrayBuffer, donc la mutabilité des collections ici n'est pas vraiment pertinente. Ce n'est donc pas une bonne réponse parce que ce n'est pas ce dont parle le PO. Bien que ce soit un bon exemple des dangers de transmettre une collection mutable si vous ne l'aviez pas l'intention de le faire.
EdgeCaseBerg
@EdgeCaseBerg Mais vous ne pouvez pas reproduire le comportement que j'obtiens ArrayBufferen utilisant Vector. La question du PO est large, mais ils cherchaient des suggestions sur le moment de l'utiliser, donc je pense que ma réponse est utile car elle illustre les dangers de transmettre une collection mutable (le fait que cela valn'aide pas); immutable varest plus sûr que mutable val.
Akavall