На предыдущих страницах объясняется, почему предпочитают vals, неизменные объекты и методы без побочных эффектов, поэтому это предложение имеет смысл.
Но второе предложение: "Используйте vars, изменяемые объекты и методы с побочными эффектами, когда у вас есть определенная потребность и оправдание для них". не объясняется так хорошо.
P.s: Было бы здорово, если бы кто-то мог привести несколько примеров для каждого из них (помимо объяснения).
Ответ 2
Простота незначительных обновлений
Одной из причин использования изменчивости является то, что вы отслеживаете некоторые текущие процессы. Например, допустим, что я редактирую большой документ и имею сложный набор классов для отслеживания различных элементов текста, истории редактирования, положения курсора и т.д. Теперь предположим, что пользователь нажимает на другую часть текста. Восстановить объект документа, скопировать много полей, но не в поле EditState
; воссоздайте EditState
с новыми ViewBounds
и documentCursorPosition
? Или я изменяю изменяемую переменную в одном месте? Пока безопасность потоков не является проблемой, гораздо проще и меньше подвержено ошибкам, чтобы просто обновить переменную или две, чем скопировать все. Если проблема безопасности потоков является проблемой, защита от параллельного доступа может быть более эффективной, чем использование неизменяемого подхода и устранение устаревших запросов.
Эффективность вычислений
Другой причиной использования изменчивости является скорость. Создание объекта дешево, но простые вызовы методов дешевле, а операции с примитивными типами еще дешевле.
Предположим, например, что мы имеем отображение, и мы хотим суммировать значения и квадраты значений.
val xs = List.range(1,10000).map(x => x.toString -> x).toMap
val sum = xs.values.sum
val sumsq = xs.values.map(x => x*x).sum
Если вы делаете это каждый раз в то время, это не имеет большого значения. Но если вы обратите внимание на то, что происходит, для каждого элемента списка вы сначала воссоздаете его (значения), затем суммируете его (в коробке), затем заново создавайте его (значения), а затем снова создавайте его в квадратной форме с боксом (картой), затем суммируйте его. Это, по крайней мере, шесть созданий объектов и пять полных обходов только для того, чтобы сделать два добавления и один умножить на элемент. Невероятно неэффективно.
Вы можете попытаться добиться большего, избегая множественной рекурсии и проходя через карту только один раз, используя сгиб:
val (sum,sumsq) = ((0,0) /: xs){ case ((sum,sumsq),(_,v)) => (sum + v, sumsq + v*v) }
И это намного лучше, примерно на 15 раз лучше, чем на моей машине. Но у вас все еще есть три создания объектов на каждой итерации. Если вместо этого вы
case class SSq(var sum: Int = 0, var sumsq: Int = 0) {
def +=(i: Int) { sum += i; sumsq += i*i }
}
val ssq = SSq()
xs.foreach(x => ssq += x._2)
вы снова в два раза быстрее, потому что вы сокращаете бокс. Если у вас есть данные в массиве и используйте цикл while, то вы можете избежать создания и бокса объектов и ускорить еще на 20%.
Теперь, если вы сказали, вы могли бы также выбрать рекурсивную функцию для вашего массива:
val ar = Array.range(0,10000)
def suma(xs: Array[Int], start: Int = 0, sum: Int = 0, sumsq: Int = 0): (Int,Int) = {
if (start >= xs.length) (sum, sumsq)
else suma(xs, start+1, sum+xs(start), sumsq + xs(start)*xs(start))
}
и написано так же быстро, как изменчивый SSq. Но если мы сделаем это:
def sumb(xs: Array[Int], start: Int = 0, ssq: (Int,Int) = (0,0)): (Int,Int) = {
if (start >= xs.length) ssq
else sumb(xs, start+1, (ssq._1+xs(start), ssq._2 + xs(start)*xs(start)))
}
мы теперь на 10 раз медленнее, потому что нам нужно создать объект на каждом шаге.
Итак, суть в том, что на самом деле имеет значение только то, что у вас есть неизменность, когда вы не можете удобно переносить свою структуру обновления вместе с независимыми аргументами в метод. Как только вы выйдете за пределы сложности, где это работает, изменчивость может стать большой победой.
Создание совокупного объекта
Если вам нужно создать сложный объект с полями n
из потенциально поврежденных данных, вы можете использовать шаблон построителя, который выглядит так:
abstract class Built {
def x: Int
def y: String
def z: Boolean
}
private class Building extends Built {
var x: Int = _
var y: String = _
var z: Boolean = _
}
def buildFromWhatever: Option[Built] = {
val b = new Building
b.x = something
if (thereIsAProblem) return None
b.y = somethingElse
// check
...
Some(b)
}
Это работает только с изменяемыми данными. Конечно, есть и другие варианты:
class Built(val x: Int = 0, val y: String = "", val z: Boolean = false) {}
def buildFromWhatever: Option[Built] = {
val b0 = new Built
val b1 = b0.copy(x = something)
if (thereIsAProblem) return None
...
Some(b)
}
который во многих отношениях еще более чист, за исключением того, что вы должны копировать свой объект один раз для каждого изменения, которое вы делаете, что может быть болезненно медленным. И ни один из них не является особенно пуленепробиваемым; для этого вы, вероятно, захотите
class Built(val x: Int, val y: String, val z: Boolean) {}
class Building(
val x: Option[Int] = None, val y: Option[String] = None, val z: Option[Boolean] = None
) {
def build: Option[Built] = for (x0 <- x; y0 <- y; z0 <- z) yield new Built(x,y,z)
}
def buildFromWhatever: Option[Build] = {
val b0 = new Building
val b1 = b0.copy(x = somethingIfNotProblem)
...
bN.build
}
но опять-таки много накладных расходов.