Scala 中 val 可变与 var 不可变

Posted

技术标签:

【中文标题】Scala 中 val 可变与 var 不可变【英文标题】:val-mutable versus var-immutable in Scala 【发布时间】:2012-07-08 08:24:19 【问题描述】:

Scala 中是否有关于何时将 val 与可变集合一起使用以及何时将 var 与不可变集合一起使用的指南?或者你真的应该以不可变集合的 val 为目标吗?

这两种类型的收藏都给了我很多选择,但我通常不会 知道如何做出选择。

【问题讨论】:

参见例如***.com/questions/10999024/… 【参考方案1】:

很常见的问题,这个。最难的是找到重复项。

您应该争取参考透明度。这意味着,如果我有一个表达式“e”,我可以创建一个val x = e,并将e 替换为x。这是可变性破坏的属性。每当您需要做出设计决策时,请最大限度地提高参考透明度。

实际上,方法本地var 是最安全的var,因为它不会逃避方法。如果方法短一点,那就更好了。如果不是,请尝试通过提取其他方法来减少它。

另一方面,可变集合有可能逃逸,即使它没有逃逸。更改代码时,您可能希望将其传递给其他方法,或将其返回。这就是破坏参照透明度的事情。

在一个对象(一个字段)上,几乎会发生同样的事情,但会产生更可怕的后果。无论哪种方式,对象都将具有状态,因此会破坏引用透明度。但是拥有一个可变集合意味着即使是对象本身也可能无法控制谁在更改它。

【讨论】:

很好,我脑海中的新大图景:更喜欢immutable val 胜过immutable var 胜过mutable val 胜过mutable var。尤其是immutable var 超过mutable val 请记住,您仍然可以通过本地可变 var 关闭(如泄漏可以更改它的副作用“函数”)。使用不可变集合的另一个不错的功能是您可以有效地保留旧副本,即使 var 发生变异。 tl;dr: 更喜欢var x: Set[Int] over val x: mutable.Set[Int] 因为如果您将x 传递给其他函数,在前一种情况下,您可以肯定,该函数不能为你改变x【参考方案2】:

如果您使用不可变集合并且需要“修改”它们,例如,在循环中向它们添加元素,那么您必须使用vars,因为您需要将生成的集合存储在某个地方。如果您只读取不可变集合,请使用vals。

一般来说,请确保不要混淆引用和对象。 vals 是不可变引用(C 中的常量指针)。也就是说,当您使用val x = new MutableFoo() 时,您将能够更改x 指向的对象,但您将无法更改为哪个对象强>x点。如果您使用var x = new ImmutableFoo(),则相反。采纳我最初的建议:如果您不需要更改参考点到哪个对象,请使用vals。

【讨论】:

var immutable = something(); immutable = immutable.update(x) 违背了使用不可变集合的目的。您已经放弃了引用透明度,并且通常可以从具有更好时间复杂度的可变集合中获得相同的效果。在四种可能性(valvar,可变和不可变)中,这个最没有意义。我经常使用val mutable @JimPivarski 我不同意,其他人也一样,请参阅丹尼尔的回答和彼得的评论。如果您需要更新数据结构,那么使用不可变 var 而不是可变 val 的优点是您可以泄漏对结构的引用,而不会冒着被其他人修改而破坏本地假设的风险。这些“其他人”的缺点是,他们可能会读取过时的数据。 我改变了主意,我同意你的看法(我将把我原来的评论留给历史)。从那以后我就使用了这个,尤其是在var list: List[X] = Nil; list = item :: list; ... 中,我忘记了我曾经写过不同的东西。 @MalteSchwerhoff:“陈旧数据”实际上是可取的,这取决于您如何设计程序,如果一致性至关重要;例如,这是 Clojure 中并发如何工作的主要基本原则之一。 @ErikAllik 我不会说陈旧的数据本身就是可取的,但我同意它可以完全没问题,这取决于您想要/需要向客户提供的保证。或者你有没有一个例子,读取陈旧数据的唯一事实实际上是一个优势?我不是指接受陈旧数据的后果,这可能是更好的性能或更简单的 API。【参考方案3】:

回答这个问题的最佳方法是举个例子。假设我们有一些过程只是出于某种原因收集数字。我们希望记录这些数字,并将集合发送到另一个进程来执行此操作。

当然,在将集合发送到记录器之后,我们仍在收集数字。假设在日志记录过程中有一些开销会延迟实际的日志记录。希望你能看到这是怎么回事。

如果我们将此集合存储在一个可变的val 中(可变的,因为我们不断地添加它),这意味着执行日志记录的进程将查看仍然存在的相同的对象正在通过我们的收集过程进行更新。该集合可能随时更新,因此当需要记录时,我们可能实际上并未记录我们发送的集合。

如果我们使用不可变的var,我们会向记录器发送一个不可变的数据结构。当我们向集合中添加更多数字时,我们将替换我们的var新的不可变数据结构。这并不意味着发送到记录器的集合被替换!它仍在引用它发送的集合。所以我们的记录器确实会记录它收到的集合。

【讨论】:

【参考方案4】:

我认为这篇博文中的示例会更清楚地说明问题,因为在并发场景中使用哪个组合变得更加重要:importance of immutability for concurrency。在我们讨论的同时,请注意 synchronized 与 @volatile 与 AtomicReference 之类的首选用法:three tools

【讨论】:

【参考方案5】:

var immutableval 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,我在第二个示例中有意使用向量,因为我试图表明第一个示例mutable val 的行为不适用于immutable var。这里有什么不正确的? 您在这里将苹果与橙子进行比较。 Vector 没有像数组缓冲区这样的+= 方法。您的回答暗示 +=x = x + y 相同,但事实并非如此。您将函数参数视为 vals 的声明是正确的,并且您确实收到了您提到的错误,但这只是因为您使用了=。您可以使用 ArrayBuffer 得到相同的错误,因此这里的集合可变性并不真正相关。所以这不是一个好的答案,因为它没有理解 OP 在说什么。虽然这是一个很好的例子,说明如果你不打算传递一个可变集合是危险的。 @EdgeCaseBerg 但是您无法通过使用Vector 来复制我使用ArrayBuffer 得到的行为。 OP 的问题很广泛,但他们正在寻找关于何时使用 which 的建议,所以我相信我的回答很有用,因为它说明了传递可变集合的危险(val 的事实无济于事); immutable varmutable val 更安全。

以上是关于Scala 中 val 可变与 var 不可变的主要内容,如果未能解决你的问题,请参考以下文章

Scala 基础

Scala 集合中的可变与不可变

2.Scala-基础

2021年大数据常用语言Scala(十八):基础语法学习 Map对象

Scala 变量

Programming In Scala笔记-第二三章