val-mutable versus var-immutable w Scali

99

Czy w Scali są jakieś wytyczne dotyczące tego, kiedy używać val ze zmienną kolekcją, a kiedy var z niezmienną kolekcją? A może naprawdę powinieneś dążyć do val z niezmienną kolekcją?

Fakt, że istnieją oba rodzaje kolekcji, daje mi duży wybór, a często nie wiem, jak tego dokonać.

James McCabe
źródło
Zobacz na przykład stackoverflow.com/questions/10999024/…
Luigi Plinge

Odpowiedzi:

105

To dość powszechne pytanie. Trudno jest znaleźć duplikaty.

Należy dążyć do referencyjnej przejrzystości . Co to znaczy, że jeśli mam wyrażenie „e”, mogę zrobić val x = ei wymienić ez x. To jest właściwość, którą łamie zmienność. Zawsze, gdy musisz podjąć decyzję projektową, maksymalizuj ją, aby uzyskać referencyjną przejrzystość.

Z praktycznego punktu widzenia metoda lokalna varjest najbezpieczniejsza z varistniejących, ponieważ nie wymyka się metodzie. Jeśli metoda jest krótka, jeszcze lepsza. Jeśli tak nie jest, spróbuj go zmniejszyć, wyodrębniając inne metody.

Z drugiej strony zmienna kolekcja ma potencjał do ucieczki, nawet jeśli tak się nie stanie. Zmieniając kod, możesz chcieć przekazać go innym metodom lub zwrócić. To jest rodzaj rzeczy, która łamie referencyjną przejrzystość.

Na obiekcie (polu) dzieje się prawie to samo, ale z bardziej tragicznymi konsekwencjami. Tak czy inaczej obiekt będzie miał stan, a zatem złamie przezroczystość referencyjną. Ale posiadanie zmiennej kolekcji oznacza, że ​​nawet sam obiekt może stracić kontrolę nad tym, kto go zmienia.

Daniel C. Sobral
źródło
39
Ładny, nowy duży obraz w moim umyśle: Wolę immutable valponad immutable varponad mutable valponad mutable var. Zwłaszcza immutable varkoniec mutable val!
Peter Schmitz,
2
Pamiętaj, że nadal możesz zamknąć (tak jak w przypadku wycieku "funkcję" powodującą efekt uboczny, która może ją zmienić) nad lokalną zmienną var. Inną fajną cechą korzystania z niezmiennych kolekcji jest to, że możesz skutecznie przechowywać stare kopie, nawet jeśli są varmutacje.
Tajemniczy Dan
1
tl; dr: prefer var x: Set[Int] over, val x: mutable.Set[Int] ponieważ jeśli przejdziesz xdo jakiejś innej funkcji, w pierwszym przypadku jesteś pewien, że ta funkcja nie może zostać xza Ciebie zmieniona .
pathikrit
17

Jeśli pracujesz z niezmiennymi kolekcjami i musisz je „zmodyfikować”, na przykład dodać do nich elementy w pętli, musisz użyć vars, ponieważ musisz gdzieś przechowywać wynikową kolekcję. Jeśli czytasz tylko z niezmiennych kolekcji, użyj vals.

Ogólnie upewnij się, że nie pomylisz odniesień i obiektów. vals są niezmiennymi referencjami (stałe wskaźniki w C). Oznacza to, że kiedy używasz val x = new MutableFoo(), będziesz mógł zmienić obiekt, który xwskazuje, ale nie będziesz w stanie zmienić tego, który obiekt x wskazuje. Odwrotnie jest, jeśli używasz var x = new ImmutableFoo(). Podchodzę do mojej wstępnej rady: jeśli nie musisz zmieniać obiektu, do którego odnosi się punkt odniesienia, użyj vals.

Malte Schwerhoff
źródło
1
var immutable = something(); immutable = immutable.update(x)pokonuje cel używania niezmiennej kolekcji. Zrezygnowałeś już z referencyjnej przezroczystości i zwykle możesz uzyskać ten sam efekt ze zmiennej kolekcji o większej złożoności czasowej. Z czterech możliwości ( vali var, zmienna i niezmienna), ta ma najmniej sensu. Często używam val mutable.
Jim Pivarski,
3
@JimPivarski Nie zgadzam się, podobnie jak inni, widzę odpowiedź Daniela i komentarz Piotra. Jeśli musisz zaktualizować strukturę danych, użycie niezmiennej zmiennej zamiast zmiennej zmiennej ma tę zaletę, że możesz wyciekać odniesienia do struktury bez ryzyka, że ​​zostanie ona zmodyfikowana przez innych w sposób, który łamie Twoje lokalne założenia. Wadą tych „innych” jest to, że mogą odczytywać nieaktualne dane.
Malte Schwerhoff
Zmieniłem zdanie i zgadzam się z Tobą (oryginalny komentarz zostawiam do historii). Od tego czasu używam tego, zwłaszcza w var list: List[X] = Nil; list = item :: list; ...i zapomniałem, że kiedyś pisałem inaczej.
Jim Pivarski
@MalteSchwerhoff: „nieaktualne dane” są właściwie pożądane, w zależności od tego, jak zaprojektowałeś swój program, jeśli spójność jest kluczowa; jest to na przykład jedna z głównych podstawowych zasad działania współbieżności w Clojure.
Erik Kaplun
@ErikAllik Nie powiedziałbym, że nieaktualne dane są same w sobie pożądane, ale zgadzam się, że może to być całkowicie w porządku, w zależności od gwarancji, które chcesz / musisz dać swoim klientom. A może masz przykład, w którym sam fakt odczytu nieaktualnych danych jest w rzeczywistości zaletą? Nie mam na myśli konsekwencji przyjmowania nieaktualnych danych, którymi może być lepsza wydajność lub prostszy interfejs API.
Malte Schwerhoff
9

Najlepszym sposobem odpowiedzi na to jest przykład. Załóżmy, że z jakiegoś powodu mamy jakiś proces po prostu zbierający liczby. Chcemy zarejestrować te numery i wyślemy kolekcję do innego procesu, aby to zrobić.

Oczywiście po wysłaniu kolekcji do rejestratora nadal zbieramy numery. Załóżmy, że proces rejestrowania jest obciążony, co opóźnia faktyczne rejestrowanie. Mam nadzieję, że widzisz, dokąd to zmierza.

Jeśli przechowujemy tę kolekcję jako zmienną val(zmienną, ponieważ stale do niej dodajemy), oznacza to, że proces rejestrujący będzie szukał tego samego obiektu, który wciąż jest aktualizowany przez nasz proces zbierania. Ta kolekcja może zostać zaktualizowana w dowolnym momencie, więc kiedy nadejdzie czas na rejestrację, możemy w rzeczywistości nie rejestrować kolekcji, którą wysłaliśmy.

Jeśli używamy niezmiennej var, wysyłamy niezmienną strukturę danych do rejestratora. Kiedy dodać więcej numerów do naszej kolekcji, będziemy zastępując Our varz nowym niezmiennej struktury danych . Nie oznacza to, że zbiór przesłany do rejestratora zostanie zastąpiony! Nadal odwołuje się do kolekcji, którą wysłał. Zatem nasz logger rzeczywiście zarejestruje otrzymaną kolekcję.

jmazin
źródło
1

Myślę, że przykłady w tym wpisie na blogu rzucą więcej światła, ponieważ kwestia, którego kombinacji użyć, staje się jeszcze ważniejsza w scenariuszach współbieżności: znaczenie niezmienności dla współbieżności . A skoro już o tym mowa, zwróć uwagę na preferowane użycie synchronized vs @volatile vs czegoś takiego jak AtomicReference: trzy narzędzia

Bogdan Nicolau
źródło
-2

var immutable vs. val mutable

Oprócz wielu doskonałych odpowiedzi na to pytanie. Oto prosty przykład, który ilustruje potencjalne zagrożenia val mutable:

Mutowalne obiekty można modyfikować wewnątrz metod, które przyjmują je jako parametry, podczas gdy ponowne przypisywanie jest niedozwolone.

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

Wynik:

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

Coś takiego nie może się zdarzyć var immutable, ponieważ zmiana przypisania jest niedozwolona:

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

Prowadzi do:

error: reassignment to val

Ponieważ parametry funkcji są traktowane jako valtakie ponowne przypisanie, nie jest dozwolone.

Akavall
źródło
To jest niepoprawne. Powodem tego błędu jest to, że w drugim przykładzie użyłeś Vector, który domyślnie jest niezmienny. Jeśli używasz ArrayBuffer, zobaczysz, że kompiluje się dobrze i robi to samo, co po prostu dodaje nowy element i drukuje zmutowany bufor. pastebin.com/vfq7ytaD
EdgeCaseBerg
@EdgeCaseBerg, celowo używam Vectora w moim drugim przykładzie, ponieważ próbuję pokazać, że zachowanie pierwszego przykładu mutable valnie jest możliwe w przypadku immutable var. Co tutaj jest nieprawidłowe?
Akavall
Porównujesz tutaj jabłka do pomarańczy. Wektor nie ma +=metody takiej jak bufor tablicy. Twoje odpowiedzi sugerują, że +=jest to to samo, x = x + yco nie. Twoje stwierdzenie, że parametry funkcji są traktowane jako wartości vals, jest poprawne i pojawia się błąd, o którym wspomniałeś, ale tylko dlatego, że użyłeś =. Ten sam błąd można uzyskać w przypadku obiektu ArrayBuffer, więc zmienność kolekcji tutaj nie jest tak naprawdę istotna. Więc to nie jest dobra odpowiedź, ponieważ nie dociera do tego, o czym mówi OP. Chociaż jest to dobry przykład niebezpieczeństw związanych z przekazywaniem zmiennej kolekcji, jeśli nie zamierzałeś tego robić.
EdgeCaseBerg
@EdgeCaseBerg Ale nie możesz odtworzyć zachowania, które mam ArrayBuffer, używając Vector. Pytanie PO jest szerokie, ale szukali sugestii, kiedy użyć którego, więc uważam, że moja odpowiedź jest przydatna, ponieważ ilustruje niebezpieczeństwa związane z przekazywaniem mutowalnej kolekcji (fakt, że to valnie pomaga); immutable varjest bezpieczniejszy niż mutable val.
Akavall