¿Cuál es la diferencia entre una definición de var y val en Scala?

Resuelto Derek Mahar asked hace 14 años • 13 respuestas

¿Cuál es la diferencia entre una definición vary valen Scala y por qué el lenguaje necesita ambas? ¿Por qué elegirías a valen lugar de a vary viceversa?

Derek Mahar avatar Nov 24 '09 23:11 Derek Mahar
Aceptado

Como han dicho muchos otros, el objeto asignado a a valno se puede reemplazar, y el objeto asignado a a no se varpuede reemplazar. Sin embargo, a dicho objeto se le puede modificar su estado interno. Por ejemplo:

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.
  }
}

Entonces, aunque no podamos cambiar el objeto asignado a x, podríamos cambiar el estado de ese objeto. Sin embargo, en el fondo había un var.

Ahora bien, la inmutabilidad es algo bueno por muchas razones. Primero, si un objeto no cambia su estado interno, no tiene que preocuparse si alguna otra parte de su código lo está cambiando. Por ejemplo:

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

Esto se vuelve particularmente importante con los sistemas multiproceso. En un sistema multiproceso, puede suceder lo siguiente:

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

Si usa valexclusivamente y solo usa estructuras de datos inmutables (es decir, evita matrices, todo lo que está en scala.collection.mutable, etc.), puede estar seguro de que esto no sucederá. Es decir, a menos que haya algún código, tal vez incluso un marco, que haga trucos de reflexión; desafortunadamente, la reflexión puede cambiar valores "inmutables".

Ésa es una razón, pero hay otra razón. Cuando usa var, puede verse tentado a reutilizar el mismo varpara múltiples propósitos. Esto tiene algunos problemas:

  • Será más difícil para las personas que lean el código saber cuál es el valor de una variable en una determinada parte del código.
  • Es posible que olvide reinicializar la variable en alguna ruta de código y termine pasando valores incorrectos en el código.

En pocas palabras, el uso vales más seguro y genera un código más legible.

Entonces podemos ir en la otra dirección. Si valeso es mejor, ¿por qué tenerlo var? Bueno, algunos lenguajes tomaron ese camino, pero hay situaciones en las que la mutabilidad mejora mucho el rendimiento.

Por ejemplo, tomemos un inmutable Queue. Cuando colocas alguna enqueuede dequeuelas cosas en él, obtienes un nuevo Queueobjeto. Entonces, ¿cómo procederías para procesar todos los elementos que contiene?

Lo explicaré con un ejemplo. Digamos que tienes una cola de dígitos y quieres componer un número a partir de ellos. Por ejemplo, si tengo una cola con 2, 1, 3, en ese orden, quiero recuperar el número 213. Primero resolvámoslo con mutable.Queue:

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

Este código es rápido y fácil de entender. Su principal inconveniente es que la cola que se pasa la modifica toNum, por lo que hay que hacer una copia de ella previamente. Ese es el tipo de gestión de objetos del que te libera la inmutabilidad.

Ahora, convirtámoslo en 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)
}

Como no puedo reutilizar alguna variable para realizar un seguimiento de mi archivo num, como en el ejemplo anterior, necesito recurrir a la recursividad. En este caso, se trata de una recursividad de cola, que tiene un rendimiento bastante bueno. Pero ese no es siempre el caso: a veces simplemente no existe una buena solución de recursividad de cola (legible y simple).

Sin embargo, tenga en cuenta que puedo reescribir ese código para usar an immutable.Queuey a varal mismo tiempo. Por ejemplo:

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
}

Este código sigue siendo eficiente, no requiere recursividad y no necesita preocuparse si tiene que hacer una copia de su cola o no antes de llamar toNum. Naturalmente, evité reutilizar variables para otros propósitos, y ningún código fuera de esta función las ve, por lo que no necesito preocuparme de que sus valores cambien de una línea a la siguiente, excepto cuando lo hago explícitamente.

Scala optó por dejar que el programador hiciera eso, si éste consideraba que era la mejor solución. Otros lenguajes han optado por dificultar dicho código. El precio que paga Scala (y cualquier lenguaje con mutabilidad generalizada) es que el compilador no tiene tanta libertad para optimizar el código como podría de otra manera. La respuesta de Java a esto es optimizar el código según el perfil de tiempo de ejecución. Podríamos seguir y seguir sobre los pros y los contras de cada lado.

Personalmente, creo que Scala logra el equilibrio adecuado, por ahora. No es perfecto, ni de lejos. Creo que tanto Clojure como Haskell tienen nociones muy interesantes que Scala no adoptó, pero Scala también tiene sus propios puntos fuertes. Veremos qué surge en el futuro.

Daniel C. Sobral avatar Nov 24 '2009 19:11 Daniel C. Sobral

vales final, es decir, no se puede configurar. Piensa finalen java.

Jackson Davis avatar Nov 24 '2009 16:11 Jackson Davis