在Scala中,val-mutable与var-immutable有什么区别?

111

在Scala中,是否有关于何时使用可变集合的val与不可变集合的var的指导方针?或者您真的应该使用不可变集合的val?

事实上,存在两种类型的集合给了我很多选择,但通常我不知道如何做出选择。


请参见https://dev59.com/sWgu5IYBdhLWcg3w5K9a#11002321。 - Luigi Plinge
5个回答

107

这是一个相当常见的问题。难点在于找出重复项。

你应该追求 引用透明度。这意味着,如果我有一个表达式“e”,我可以创建一个val x = e,并用x代替e。这就是可变性破坏的属性。每当你需要做出设计决策时,请最大化引用透明度。

从实际角度来看,方法局部的var是最安全的var,因为它不会逃出方法。如果方法很短,那就更好了。如果不是,尝试通过提取其他方法来减少方法长度。

另一方面,可变集合有可能会逃逸,即使它并没有逃逸。当更改代码时,您可能想将其传递给其他方法或返回它。这就是破坏引用透明度的类型。

在对象(字段)上,发生的事情基本相同,但后果更加严重。无论哪种方式,对象都将具有状态,因此会破坏引用透明度。但有了可变集合,甚至对象本身也可能失去控制权。


39
我会尽力为您翻译,以下是翻译的结果:在我心中有一个很好的新图像:更喜欢使用immutable val,而不是immutable var,更喜欢使用mutable val,而不是mutable var,特别是比mutable val更喜欢使用immutable var - Peter Schmitz
2
请记住,您仍然可以关闭(例如泄漏具有副作用的“函数”以更改它)本地可变的var。使用不可变集合的另一个好处是,即使var发生变化,您也可以有效地保留旧副本。 - Mysterious Dan
1
简而言之:优先使用var x: Set[Int],而不是val x: mutable.Set[Int],因为在前者的情况下,如果将x传递给其他函数,则可以确保该函数无法更改x - pathikrit

18
如果您使用不可变集合并需要对其进行“修改”,例如在循环中添加元素,则必须使用var,因为您需要在某个地方存储结果集合。如果您仅从不可变集合中读取,则请使用val
一般来说,请确保不要混淆引用和对象,val是不可变的引用(C语言中的常量指针)。也就是说,当您使用val x = new MutableFoo()时,您可以更改x指向的对象,但您将无法更改x指向的对象。如果您不需要更改引用所指向的对象,请使用val

1
var immutable = something(); immutable = immutable.update(x) 这样做违背了使用不可变集合的初衷。您已经放弃了引用透明性,并且通常可以从具有更好时间复杂度的可变集合中获得相同的效果。在四种可能性(valvar,可变和不可变)中,这种方法是最没有意义的。我经常使用 val mutable - Jim Pivarski
3
我不同意JimPivarski的观点,其他人也是这样认为的,可以参考Daniel的回答和Peter的评论。如果需要更新数据结构,使用不可变的var而不是可变的val会有优势,因为你可以泄露对该结构的引用,而不必担心他人以破坏你的本地假设的方式修改它。但“其他人”的缺点是,他们可能会读取旧数据。 - Malte Schwerhoff
@ErikAllik 我不会说陈旧的数据本身是可取的,但我同意它可以完全没问题,这取决于您想要/需要向客户提供的保证。或者您是否有一个例子,仅仅读取陈旧的数据实际上是一种优势?我不是指接受陈旧数据的后果,这可能是更好的性能或更简单的API。 - Malte Schwerhoff
在具有时间方面的数据库的高并发应用程序中,“陈旧数据”更好:想象一下,您有一个实体A,您想对其进行某些计算,并将结果与之关联;如果现在有什么改变了A的全局可见状态,则关联将不一致;而如果计算始终看到并使用A的“陈旧”快照,则一切都将保持一致,并且A的下一个快照将具有自己的计算结果。基本上,您只是为了保持一致性而在短时间内忽略了时间的流逝。 - Erik Kaplun
@MalteSchwerhoff:或者直接跳过我写的内容,阅读有关例如Clojure背后的概念和数学。 - Erik Kaplun
显示剩余2条评论

10
最好的回答是通过一个示例来说明。假设我们有一个过程仅为某种原因收集数字。我们希望记录这些数字,并将收集发送到另一个进程进行记录。
当然,在我们将收集发送到记录器之后,我们仍在收集数字。并且假设记录过程中存在一些开销会延迟实际记录时间。希望您能看出这个问题所在。
如果我们将此收集存储在可变的val中(可变是因为我们正在不断添加它),这意味着执行记录的进程将查看仍在被我们的收集过程更新的相同对象。该收集可能随时更新,因此在记录时,我们可能实际上没有记录发送的收集。
如果我们使用不可变的var,我们向记录器发送不可变数据结构。当我们向我们的收集添加更多数字时,我们将使用新的不可变数据结构替换我们的var。这并不意味着发送到记录器的收集被替换了!它仍然引用它被发送的集合。因此,我们的记录器确实会记录它收到的收集。

2
我认为这篇博客文章中的例子将更加详细地阐明问题,因为在并发场景下,选择哪种组合变得更加重要:不可变性对并发的重要性。顺便提一下,在此过程中,请注意优先使用synchronized vs @volatile vs 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,你会看到它可以编译通过,并且做了相同的事情,只是添加了新元素并打印出改变后的缓冲区。https://pastebin.com/vfq7ytaD - EdgeCaseBerg
@EdgeCaseBerg,我在第二个例子中故意使用了Vector,因为我想展示第一个例子mutable val的行为是不可能用immutable var实现的。这里有什么错误吗? - Akavall
在这里进行了不恰当的比较。向量没有 += 方法,就像阵列缓冲区一样。你的答案暗示着 +=x = x + y 是相同的,但实际并不是这样。你关于函数参数被视为 vals 的说法是正确的,并且你确实会得到你提到的错误,但这仅仅是因为你使用了 =。你可以通过 ArrayBuffer 来获得同样的错误,所以这里集合的可变性并不是很相关。因此,这不是一个很好的答案,因为它没有涉及到 OP 所讨论的问题。虽然这是一个很好的例子,说明传递一个可变集合的危险性,如果你没有打算这样做的话。 - EdgeCaseBerg
@EdgeCaseBerg 但是你无法通过使用“Vector”来复制我使用“ArrayBuffer”获得的行为。OP的问题很广泛,但他们正在寻求何时使用哪个的建议,因此我认为我的答案很有用,因为它说明了传递可变集合的危险(即使是“val”也没有帮助);“immutable var”比“mutable val”更安全。 - Akavall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接