Como los val
son semánticamente estáticos, se inicializan "en el lugar" donde aparecen en el código. Esto puede producir un comportamiento sorprendente e indeseable cuando se usa en clases abstractas y rasgos.
Por ejemplo, digamos que nos gustaría hacer un rasgo llamado PlusOne
que defina una operación de incremento en un Int
envuelto. Dado que los Int
s son inmutables, el valor más uno se conoce en la inicialización y nunca se cambiará después, por lo que semánticamente es un valor val
. Sin embargo, definirlo de esta manera producirá un resultado inesperado.
trait PlusOne {
val i:Int
val incr = i + 1
}
class IntWrapper(val i: Int) extends PlusOne
No importa cuál es el valor i
se construye IntWrapper
con, llamando .incr
en el objeto devuelto siempre devuelve 1. Esto es porque el val incr
es inicializado en el rasgo, antes de la clase que se extiende, y en ese momento i
sólo tiene el valor por defecto de 0
. (En otras condiciones, puede completarse con Nil
, null
o un valor predeterminado similar).
La regla general, entonces, es evitar usar val
en cualquier valor que dependa de un campo abstracto. En su lugar, use lazy val
, que no evalúa hasta que se necesita, o def
, que evalúa cada vez que se llama. Sin embargo, tenga en cuenta que si el valor de lazy val
es forzado a evaluar por un val
antes de que se complete la inicialización, ocurrirá el mismo error.
Un violín (escrito en Scala-Js, pero se aplica el mismo comportamiento) se puede encontrar aquí.