Ответ 1
Как и многие другие, объект, назначенный val
, не может быть заменен, и объект, назначенный var
, может. Однако у указанного объекта может быть изменено его внутреннее состояние. Например:
class A(n: Int) {
var value = n
}
class B(n: Int) {
val value = new A(n)
}
object Test {
def main(args: Array[String]) {
val x = new B(5)
x = new B(6) // Doesn't work, because I can't replace the object created on the line above with this new one.
x.value = new A(6) // Doesn't work, because I can't replace the object assigned to B.value for a new one.
x.value.value = 6 // Works, because A.value can receive a new object.
}
}
Итак, хотя мы не можем изменить объект, назначенный на x
, мы могли бы изменить состояние этого объекта. Однако в его основе существовал var
.
Теперь неизменность - это хорошо по многим причинам. Во-первых, если объект не изменяет внутреннее состояние, вам не нужно беспокоиться, если какая-либо другая часть вашего кода меняет его. Например:
x = new B(0)
f(x)
if (x.value.value == 0)
println("f didn't do anything to x")
else
println("f did something to x")
Это становится особенно важным для многопоточных систем. В многопоточной системе может случиться следующее:
x = new B(1)
f(x)
if (x.value.value == 1) {
print(x.value.value) // Can be different than 1!
}
Если вы используете только val
и используете только неизменяемые структуры данных (т.е. избегайте массивов, все в scala.collection.mutable
и т.д.), вы можете быть уверены, что этого не произойдет. То есть, если нет какого-либо кода, возможно, даже рамки, делающего рефлексивные трюки - отражение может, к сожалению, изменить неизменные значения.
Это одна из причин, но есть и другая причина. Когда вы используете var
, вы можете столкнуться с повторным использованием одного и того же var
для нескольких целей. У этого есть некоторые проблемы:
- Пользователям, читающим код, будет труднее узнать, что такое значение переменной в определенной части кода.
- Вы можете забыть повторно инициализировать переменную в некотором пути кода и в конечном итоге передать неправильные значения ниже по течению в коде.
Проще говоря, использование val
более безопасно и приводит к более читаемому коду.
Мы можем тогда пойти в другом направлении. Если val
- это лучше, почему var
вообще? Ну, некоторые языки сделали этот маршрут, но есть ситуации, в которых изменчивость улучшает производительность, много.
Например, возьмите неизменяемый Queue
. Когда вы либо enqueue
, либо dequeue
вещи в нем, вы получаете новый объект Queue
. Как же тогда вы собираетесь обрабатывать все элементы в нем?
Я рассмотрю это с помощью примера. Скажем, у вас есть очередь цифр, и вы хотите составить номер из них. Например, если у меня есть очередь с 2, 1, 3, в этом порядке, я хочу вернуть номер 213. Пусть сначала разрешите его с помощью mutable.Queue
:
def toNum(q: scala.collection.mutable.Queue[Int]) = {
var num = 0
while (!q.isEmpty) {
num *= 10
num += q.dequeue
}
num
}
Этот код быстро и легко понять. Его главный недостаток заключается в том, что передаваемая очередь модифицируется toNum
, поэтому вы должны сделать ее копию заранее. То, что тип управления объектами, что неизменность делает вас свободным.
Теперь оставьте это на immutable.Queue
:
def toNum(q: scala.collection.immutable.Queue[Int]) = {
def recurse(qr: scala.collection.immutable.Queue[Int], num: Int): Int = {
if (qr.isEmpty)
num
else {
val (digit, newQ) = qr.dequeue
recurse(newQ, num * 10 + digit)
}
}
recurse(q, 0)
}
Поскольку я не могу повторно использовать некоторую переменную для отслеживания моего num
, как в предыдущем примере, мне нужно прибегнуть к рекурсии. В этом случае это хвостовая рекурсия, которая имеет неплохую производительность. Но это не всегда так: иногда нет хорошего (читаемого, простого) решения хвостовой рекурсии.
Обратите внимание, однако, что я могу переписать этот код для использования immutable.Queue
и a var
в одно и то же время! Например:
def toNum(q: scala.collection.immutable.Queue[Int]) = {
var qr = q
var num = 0
while (!qr.isEmpty) {
val (digit, newQ) = qr.dequeue
num *= 10
num += digit
qr = newQ
}
num
}
Этот код по-прежнему эффективен, не требует рекурсии, и вам не нужно беспокоиться о том, нужно ли вам делать копию вашей очереди или нет до вызова toNum
. Естественно, я избегал повторного использования переменных для других целей, и никакой код вне этой функции не видит их, поэтому мне не нужно беспокоиться о том, что их значения меняются от одной строки к другой, за исключением случаев, когда я явно делаю это.
Scala решил, чтобы программист сделал это, если программист счел это лучшим решением. Другие языки решили сделать такой код сложным. Цена Scala (и любой язык с широко распространенной изменчивостью) платит, так это то, что у компилятора не так много возможностей для оптимизации кода, как в противном случае. Ответ Java на это - оптимизация кода на основе профиля времени выполнения. Мы могли бы продолжать и обсуждать все плюсы и минусы с каждой стороны.
Лично я думаю, что Scala покажет правильный баланс. Это далеко не идеальный вариант. Я думаю, что Clojure и Haskell имеют очень интересные понятия, не принятые Scala, но Scala имеет свои сильные стороны. Мы увидим, что происходит в будущем.