val-mutable против var-immutable в Scala

99

Есть ли в Scala какие-либо рекомендации относительно того, когда использовать val с изменяемой коллекцией по сравнению с использованием var с неизменяемой коллекцией? Или вам действительно стоит стремиться к val с неизменной коллекцией?

Тот факт, что существуют оба типа коллекции, дает мне большой выбор, и часто я не знаю, как сделать этот выбор.

Джеймс МакКейб
источник
См., Например, stackoverflow.com/questions/10999024/…
Луиджи Плиндж 08

Ответы:

105

Довольно распространенный вопрос. Труднее всего найти дубликаты.

Вы должны стремиться к ссылочной прозрачности . Это означает, что, если у меня есть выражение «е», я могу составить a val x = eи заменить eна x. Это свойство, которое нарушает изменчивость. Всякий раз, когда вам нужно принять дизайнерское решение, максимизируйте ссылочную прозрачность.

С практической точки зрения, локальный метод varявляется наиболее безопасным из varсуществующих, поскольку он не ускользает от метода. Если способ короткий, то еще лучше. Если это не так, попробуйте уменьшить его, извлекая другие методы.

С другой стороны, изменчивая коллекция может ускользнуть, даже если этого не произойдет. При изменении кода вы можете затем передать его другим методам или вернуть. Такие вещи нарушают ссылочную прозрачность.

С объектом (полем) происходит примерно то же самое, но с более ужасными последствиями. В любом случае объект будет иметь состояние и, следовательно, нарушить ссылочную прозрачность. Но наличие изменяемой коллекции означает, что даже сам объект может потерять контроль над тем, кто его изменяет.

Дэниел С. Собрал
источник
39
Хороший, новая большая картина в моей голове: Предпочитает immutable valболее immutable varнад mutable valболее mutable var. Особенно immutable varкончено mutable val!
Peter Schmitz
2
Имейте в виду, что вы все равно можете закрыть (как в случае утечки побочной «функции», которая может ее изменить) через локальный изменяемый объект var. Еще одна приятная особенность использования неизменяемых коллекций заключается в том, что вы можете эффективно хранить старые копии, даже если они varвидоизменяются.
Mysterious Dan
1
ТЛ; др: предпочитаю var x: Set[Int] более val x: mutable.Set[Int] , так как если вы передадите xв какую - то другую функцию, в первом случае, вы уверены, что функция не может мутировать xдля вас.
патикрит
17

Если вы работаете с неизменяемыми коллекциями и вам нужно их «модифицировать», например, добавлять в них элементы в цикле, тогда вам нужно использовать vars, потому что вам нужно где-то хранить полученную коллекцию. Если вы читаете только из неизменяемых коллекций, используйте vals.

В общем, убедитесь, что вы не путаете ссылки и объекты. vals - неизменяемые ссылки (постоянные указатели в C). То есть при использовании val x = new MutableFoo()вы сможете изменить объект, на который xуказывает, но не сможете изменить, на какой объект x указывает. Обратное верно, если вы используете var x = new ImmutableFoo(). Продолжая свой первоначальный совет: если вам не нужно менять, на какой объект ориентируются, используйте vals.

Мальте Шверхофф
источник
1
var immutable = something(); immutable = immutable.update(x)побеждает цель использования неизменяемой коллекции. Вы уже отказались от ссылочной прозрачности и обычно можете получить тот же эффект от изменяемой коллекции с лучшей временной сложностью. Из четырех возможностей ( valи var, изменяемые и неизменяемые), это один делает наименьшее смысл. Я часто пользуюсь val mutable.
Джим Пиварски
3
@JimPivarski Я не согласен, как и другие, см. Ответ Дэниела и комментарий Питера. Если вам нужно обновить структуру данных, то использование неизменяемой переменной var вместо изменяемой val имеет то преимущество, что вы можете пропустить ссылки на структуру без риска того, что она будет изменена другими таким образом, чтобы нарушить ваши локальные предположения. Недостатком для этих «других» является то, что они могут читать устаревшие данные.
Malte Schwerhoff 02
Я передумал и согласен с вами (оставляю свой первоначальный комментарий для истории). С тех пор я использовал это, особенно в, var list: List[X] = Nil; list = item :: list; ...и я забыл, что когда-то писал по-другому.
Джим Пиварски 02
@MalteSchwerhoff: "устаревшие данные" на самом деле желательны, в зависимости от того, как вы разработали свою программу, если согласованность имеет решающее значение; это, например, один из основных принципов работы параллелизма в Clojure.
Эрик Каплун,
@ErikAllik Я бы не сказал, что устаревшие данные желательны сами по себе, но я согласен с тем, что это может быть прекрасно, в зависимости от гарантий, которые вы хотите / должны предоставить своим клиентам. Или у вас есть пример, в котором факт чтения устаревших данных является преимуществом? Я не имею в виду последствия приема устаревших данных, которые могут быть более высокой производительностью или более простым API.
Malte Schwerhoff
9

Лучший способ ответить на этот вопрос - привести пример. Предположим, у нас есть процесс, по какой-то причине просто собирающий числа. Мы хотим записать эти числа и отправить коллекцию в другой процесс для этого.

Конечно, мы продолжаем собирать числа после того, как отправим их в регистратор. И предположим, что в процессе регистрации есть некоторые накладные расходы, которые задерживают фактическое ведение журнала. Надеюсь, вы видите, к чему это идет.

Если мы сохраняем эту коллекцию в изменяемом val(изменяемом, потому что мы постоянно добавляем к нему), это означает, что процесс, выполняющий ведение журнала, будет смотреть на тот же объект, который все еще обновляется нашим процессом сбора. Эта коллекция может быть обновлена ​​в любое время, поэтому, когда пришло время регистрировать, мы можем фактически не регистрировать отправленную коллекцию.

Если мы используем неизменяемый var, мы отправляем неизменяемую структуру данных в регистратор. Когда мы добавляем больше чисел в нашу коллекцию, мы будем заменять OUR varс новыми непреложными структурами данных . Это не означает, что коллекция, отправленная в регистратор, заменена! Он по-прежнему ссылается на отправленную коллекцию. Таким образом, наш регистратор действительно регистрирует полученную коллекцию.

Джмазин
источник
1

Я думаю, что примеры в этом сообщении в блоге прольют больше света, поскольку вопрос о том, какую комбинацию использовать, становится еще более важным в сценариях параллелизма: важность неизменности для параллелизма . И пока мы занимаемся этим, обратите внимание на предпочтительное использование synchronized и @volatile по сравнению с чем-то вроде AtomicReference: три инструмента

Богдан Николау
источник
-2

var immutable vs. val mutable

Помимо множества отличных ответов на этот вопрос. Вот простой пример, иллюстрирующий потенциальную опасность val mutable:

Изменяемые объекты можно изменять внутри методов, которые принимают их как параметры, при этом переназначение не допускается.

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

Результат:

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

Ничего подобного не может произойти с var immutable, потому что переназначение недопустимо:

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

Результаты в:

error: reassignment to val

Поскольку параметры функции обрабатываются, valэто переназначение недопустимо.

Акавалл
источник
Это неверно. Причина, по которой вы получили эту ошибку, заключается в том, что вы использовали Vector во втором примере, который по умолчанию является неизменным. Если вы используете ArrayBuffer, вы увидите, что он компилируется нормально и делает то же самое, когда просто добавляет новый элемент и распечатывает измененный буфер. pastebin.com/vfq7ytaD
EdgeCaseBerg
@EdgeCaseBerg, я намеренно использую Vector во втором примере, потому что я пытаюсь показать, что поведение первого примера mutable valневозможно с immutable var. Что здесь неверного?
Акавалл,
Вы здесь сравниваете яблоки с апельсинами. В векторе нет такого +=метода, как буфер массива. Ваши ответы подразумевают, что +=это то же самое, x = x + yчто и не так. Ваше утверждение о том, что параметры функции обрабатываются как vals, является правильным, и вы получаете указанную ошибку, но только потому, что вы использовали =. Вы можете получить ту же ошибку с ArrayBuffer, поэтому изменчивость коллекций здесь не имеет значения. Так что это не лучший ответ, потому что он не понимает того, о чем говорит OP. Хотя это хороший пример опасности передачи изменяемой коллекции, если вы этого не собирались.
EdgeCaseBerg
@EdgeCaseBerg Но вы не можете воспроизвести поведение, которое я получаю ArrayBuffer, используя Vector. Вопрос OP обширен, но они искали предложения о том, когда использовать что, поэтому я считаю, что мой ответ полезен, потому что он иллюстрирует опасности передачи изменяемой коллекции (факт, что valэто не помогает); immutable varбезопаснее, чем mutable val.
Akavall