Scala 中的 var 和 val 定义有什么区别?

Scala 中的 var 和 val 定义有什么区别?

问题描述:

Scala 中 varval 定义之间的区别是什么?为什么该语言需要两者?为什么要选择 val 而不是 var,反之亦然?

What is the difference between a var and val definition in Scala and why does the language need both? Why would you choose a val over a var and vice versa?

正如很多人所说,分配给 val 的对象不能被替换,分配给 的对象var 可以.然而,所述对象可以修改其内部状态.例如:

As so many others have said, the object assigned to a val cannot be replaced, and the object assigned to a var can. However, said object can have its internal state modified. For example:

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.

So, even though we can't change the object assigned to x, we could change the state of that object. At the root of it, however, there was a var.

现在,出于多种原因,不变性是一件好事.首先,如果一个对象没有改变内部状态,你就不必担心代码的其他部分是否正在改变它.例如:

Now, immutability is a good thing for many reasons. First, if an object doesn't change internal state, you don't have to worry if some other part of your code is changing it. For example:

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")

这对于多线程系统变得尤为重要.在多线程系统中,可能会发生以下情况:

This becomes particularly important with multithreaded systems. In a multithreaded system, the following can happen:

x = new B(1)
f(x)
if (x.value.value == 1) {
  print(x.value.value) // Can be different than 1!
}

如果只使用val,并且只使用不可变的数据结构(即避免数组,scala.collection.mutable 中的所有内容等),则可以请放心,这不会发生.也就是说,除非有一些代码,甚至可能是一个框架,在做反射技巧——不幸的是,反射可以改变不可变"的值.

If you use val exclusively, and only use immutable data structures (that is, avoid arrays, everything in scala.collection.mutable, etc.), you can rest assured this won't happen. That is, unless there's some code, perhaps even a framework, doing reflection tricks -- reflection can change "immutable" values, unfortunately.

这是一个原因,但还有另一个原因.当您使用 var 时,您可能会出于多种目的重复使用相同的 var.这有一些问题:

That's one reason, but there is another reason for it. When you use var, you can be tempted into reusing the same var for multiple purposes. This has some problems:

  • 阅读代码的人会更难知道代码某部分中变量的值是什么.
  • 您可能忘记在某些代码路径中重新初始化变量,并最终在代码中向下游传递错误的值.

简单地说,使用 val 更安全,代码更易读.

Simply put, using val is safer and leads to more readable code.

然后,我们可以往另一个方向走.如果 val 更好,为什么还要 var 呢?嗯,有些语言确实采取了这条路线,但在某些情况下,可变性可以大大提高性能.

We can, then, go the other direction. If val is that better, why have var at all? Well, some languages did take that route, but there are situations in which mutability improves performance, a lot.

以一个不可变的Queue为例.当您在其中enqueuedequeue 事物时,您将获得一个新的Queue 对象.那么,您会如何处理其中的所有项目?

For example, take an immutable Queue. When you either enqueue or dequeue things in it, you get a new Queue object. How then, would you go about processing all items in it?

我将通过一个例子来说明.假设您有一个数字队列,并且您想从中组合一个数字.例如,如果我有一个队列,按这个顺序是 2、1、3,我想取回数字 213.让我们先用 mutable.Queue 解决它:

I'll go through that with an example. Let's say you have a queue of digits, and you want to compose a number out of them. For example, if I have a queue with 2, 1, 3, in that order, I want to get back the number 213. Let's first solve it with a mutable.Queue:

def toNum(q: scala.collection.mutable.Queue[Int]) = {
  var num = 0
  while (!q.isEmpty) {
    num *= 10
    num += q.dequeue
  }
  num
}

此代码快速且易于理解.它的主要缺点是传递的队列被toNum修改,所以你必须事先制作一个副本.这就是不变性让您摆脱的那种对象管理.

This code is fast and easy to understand. Its main drawback is that the queue that is passed is modified by toNum, so you have to make a copy of it beforehand. That's the kind of object management that immutability makes you free from.

现在,让我们把它转换成一个immutable.Queue:

Now, let's covert it to an 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,就像在前面的例子中一样,我需要求助于递归.在这种情况下,它是一个尾递归,具有相当好的性能.但情况并非总是如此:有时只是没有好的(可读的、简单的)尾递归解决方案.

Because I can't reuse some variable to keep track of my num, like in the previous example, I need to resort to recursion. In this case, it is a tail-recursion, which has pretty good performance. But that is not always the case: sometimes there is just no good (readable, simple) tail recursion solution.

但是请注意,我可以重写该代码以同时使用 immutable.Queuevar!例如:

Note, however, that I can rewrite that code to use an immutable.Queue and a var at the same time! For example:

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之前,你不用担心是否需要复制你的队列.自然地,我避免将变量重用于其他目的,并且此函数之外的任何代码都看不到它们,所以我不需要担心它们的值会从一行更改为下一行——除非我明确这样做.

This code is still efficient, does not require recursion, and you don't need to worry whether you have to make a copy of your queue or not before calling toNum. Naturally, I avoided reusing variables for other purposes, and no code outside this function sees them, so I don't need to worry about their values changing from one line to the next -- except when I explicitly do so.

Scala 选择让程序员这样做,前提是程序员认为这是最好的解决方案.其他语言选择使此类代码变得困难.Scala(以及任何具有广泛可变性的语言)所付出的代价是编译器在优化代码方面没有那么多余地.Java 对此的回答是根据运行时配置文件优化代码.我们可以继续讨论每一方的利弊.

Scala opted to let the programmer do that, if the programmer deemed it to be the best solution. Other languages have chosen to make such code difficult. The price Scala (and any language with widespread mutability) pays is that the compiler doesn't have as much leeway in optimizing the code as it could otherwise. Java's answer to that is optimizing the code based on the run-time profile. We could go on and on about pros and cons to each side.

就我个人而言,我认为 Scala 目前取得了正确的平衡.到目前为止,它并不完美.我认为 ClojureHaskell 有 Scala 没有采用的非常有趣的概念,但 Scala 也有自己的优势.我们将看到未来会发生什么.

Personally, I think Scala strikes the right balance, for now. It is not perfect, by far. I think both Clojure and Haskell have very interesting notions not adopted by Scala, but Scala has its own strengths as well. We'll see what comes up on the future.