简便的小更新
使用可变性的一个原因是如果你正在跟踪某个持续进行的过程。例如,假设我正在编辑一个大型文档,并有一组复杂的类来跟踪文本的各种元素、编辑历史、光标位置等。现在假设用户点击了文本的不同部分。我应该重新创建文档对象,复制许多字段但不包括EditState
字段;重新创建具有新ViewBounds
和documentCursorPosition
的EditState
吗?还是只在一个地方更改可变变量?只要线程安全不是问题,那么更新一个或两个变量比复制所有内容要简单得多,也更不容易出错。如果线程安全是一个问题,那么保护并发访问可能比使用不可变方法并处理过时请求更费力。
计算效率
使用可变性的另一个原因是为了速度。对象创建是廉价的,但简单的方法调用更加廉价,对基本类型的操作则更加廉价。
例如,假设我们有一个映射,我们想要对值和值的平方求和。
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
如果只是偶尔这样做,那没什么大不了的。但如果你关注正在发生的事情,对于每个列表元素,你需要首先重新创建它(values),然后对它求和(boxed),再次重新创建它(values),然后以带装箱的形式再次重新创建它(map),最后将它们相加。这至少需要六个对象的创建和五个完整的遍历,只为了每个项执行两个加法和一个乘法。非常低效。
你可以尝试通过避免多次递归并仅通过一次地传递映射来改进它,使用fold:
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
...
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
}
但是,再次强调,这会有很多开销。