2012-01-29 45 views
7

Escribir un ejemplo sencillo del libro de Odersky dio como resultado el siguiente problema:Scala cuestión de herencia: val vs def

// AbstractElement.scala 
abstract class AbstractElement { 
    val contents: Array[String] 
    val height: Int = contents.length // line 3 
} 

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { // line 6 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} 

object AbstractElement { 
    def create(ch: Char): AbstractElement = { 
    new UnifiedElement(ch, 1, 1) // line 12 
    } 
} 

,

// ElementApp.scala 
import AbstractElement.create 

object ElementApp { 

    def main(args: Array[String]): Unit = { 
    val e1 = create(' ') // line 6 
    println(e1.height) 
    } 
} 

El compilador lanza la siguiente traza:

Exception in thread "main" java.lang.NullPointerException 
    at AbstractElement.<init>(AbstractElement.scala:3) 
    at UnifiedElement.<init>(AbstractElement.scala:6) 
    at AbstractElement$.create(AbstractElement.scala:12) 
    at ElementApp$.main(ElementApp.scala:6) 
    at ElementApp.main(ElementApp.scala) 

Así que el compilador cree que el contenido sigue siendo nulo, ¡pero lo definí en UnifiedContainer!

¡Las cosas se ponen aún más raras cuando reemplazo val con def y evrth funciona perfecto!

¿Podría explicar este comportamiento?

Respuesta

13

Here es un gran artículo de Paul P que explica las complejidades de orden de inicialización en Scala. Como regla general, debe nunca usar el resumen val s. Utilice siempre los abstractos def sy lazy val s.

+2

¿Puede aclarar 'Siempre use abstract 'def's y' lazy val's? El compilador no le permite poner 'lazy val' en una clase abstracta. – jbx

+1

@jbx, sí, tienes razón. Ese solo aplica para los rasgos. – missingfaktor

+0

Algunos ejemplos que veo usan un 'val' en la clase abstracta y luego' perezoso val' en la clase concreta que lo extiende. ¿Es esta la manera correcta? (Todavía estoy aprendiendo Scala, así que me confundo un poco) – jbx

4

En la definición de AbstractElement, en la práctica se está definiendo un constructor que inicializa el contenido para anular y computa contents.length. El constructor de UnifiedElement llama al constructor AbstractElement y solo luego inicializa el contenido.

EDITAR: en otras palabras, tenemos una nueva instancia de un problema ya existente en Java (y cualquier lenguaje de programación orientada a objetos): el constructor de una superclase llama a un método implementado en una subclase, pero este último no puede ser de forma segura llamado porque la subclase aún no está construida. Los vals abstractos son solo una de las formas de activarlo.

La solución más simple aquí es simplemente hacer height un def, que es mejor anwyay, y tenga en cuenta las reglas de inicialización vinculadas en la otra respuesta.

abstract class AbstractElement { 
    val contents: Array[String] 
    def height: Int = contents.length //Make this a def 
} 

La solución ligeramente más complejo, en cambio, es la fuerza contents que ser inicializado antes altura, lo que se puede hacer con esta sintaxis:

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends { 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} with AbstractElement { 
    //... 
} 

Tenga en cuenta que la composición mixin, es decir with , no es simétrico, funciona de izquierda a derecha. Y tenga en cuenta que al final puede omitirse {}, si no define otros miembros.

Lazy vals también son una solución, pero incurren en bastante sobrecarga de tiempo de ejecución: cada vez que lee la variable, el código generado leerá un mapa de bits volátil para verificar que el campo ya se haya inicializado.

Fabricación contents a def aquí parece una mala idea, porque se recalcula con demasiada frecuencia.

Por último, evitar valores abstractos es en mi humilde opinión una medida extrema. A veces son lo correcto, solo debes tener cuidado con vals concretos que se refieren a valores abstractos.

EDIT: Parece que en lugar de un valor abstracto, uno podría usar una definición abstracta y anularla con un valor concreto. Eso es posible, pero no ayuda si hay vals concretos que se refieren a la definición abstracta. Considere esta variante del código anterior, y prestar atención a cómo se definen los miembros:

abstract class AbstractElement { 
    def contents: Array[String] 
    val height: Int = contents.length // line 3 
} 

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} 

Este código tiene el mismo comportamiento en tiempo de ejecución como el código dado por la OP, incluso si AbstractElement.contents es ahora def: el cuerpo de el descriptor de acceso lee un campo que solo se inicializa por el constructor de la subclase. La única diferencia entre un valor abstracto y una definición abstracta parece ser que un valor abstracto solo puede ser anulado por un valor concreto, por lo que puede ser útil restringir el comportamiento de las subclases si eso es lo que desea.

+0

¿Puede explicarnos por qué los abstract 'val's a veces son lo correcto? ¿Brinda alguna ventaja sobre" abstract "? def' anulado por un enfoque concreto 'val''? – missingfaktor

+0

Pensé en eso, pero no resuelve el problema, como ahora explico. – Blaisorblade