2010-11-30 6 views
38

No he podido encontrar la respuesta a esto en ninguna otra pregunta. Supongamos que tengo una superclase abstracta Abstract0 con dos subclases, Concrete1 y Concrete1. Quiero poder definir en Abstract0 algo como¿Cómo usar el tipeo de Scala, los tipos abstractos, etc. para implementar un tipo de uno mismo?

def setOption(...): Self = {...} 

donde Self sería el subtipo de hormigón. Esto permitiría que el encadenamiento de llamadas a setOption así:

val obj = new Concrete1.setOption(...).setOption(...) 

y aún así obtener hormigón1 como el tipo inferido de obj.

Lo que no quiero es definir esto:

abstract class Abstract0[T <: Abstract0[T]] 

porque hace que sea más difícil para los clientes que manejan este tipo. He intentado varias posibilidades, incluyendo un tipo abstracto:

abstract class Abstract0 { 
    type Self <: Abstract0 
} 

class Concrete1 extends Abstract0 { 
    type Self = Concrete1 
} 

pero entonces es imposible de implementar setOption, porque this en Abstract0 no tiene tipo de auto. Y usar this: Self => tampoco funciona en Abstract0.

¿Qué soluciones hay para este problema?

+0

Una opción es definir, por ejemplo, 'protegida def auto = this.asInstanceOf [Auto]' 'y luego setOption def (...) = {...; self} ', pero esto se ve un poco feo ... –

Respuesta

50

Esto es lo que this.type es para:

scala> abstract class Abstract0 { 
    | def setOption(j: Int): this.type 
    | } 
defined class Abstract0 

scala> class Concrete0 extends Abstract0 { 
    | var i: Int = 0 
    | def setOption(j: Int) = {i = j; this} 
    | } 
defined class Concrete0 

scala> (new Concrete0).setOption(1).setOption(1) 
res72: Concrete0 = [email protected] 

Como se puede ver setOption devuelve el tipo real que se utiliza, no Abstract0. Si Concrete0 tenía setOtherOption continuación (new Concrete0).setOption(1).setOtherOption(...) funcionaría

ACTUALIZACIÓN: Para responder a la pregunta de seguimiento de JPP en el comentario (cómo devolver nuevas instancias: El enfoque general que se describe en la pregunta es la correcta (usando tipos abstractos) Sin embargo, el. creación de los nuevos casos tiene que ser explícita para cada subclase

un enfoque es:..

abstract class Abstract0 { 
    type Self <: Abstract0 

    var i = 0 

    def copy(i: Int) : Self 

    def setOption(j: Int): Self = copy(j) 
} 

class Concrete0(i: Int) extends Abstract0 { 
    type Self = Concrete0 
    def copy(i: Int) = new Concrete0(i) 
} 

Otra es seguir el builder utilizado en la biblioteca de la colección de Scala es decir, setOption recibe un constructor implícito parámetro. Esto tiene Las ventajas de que la construcción de la nueva instancia se puede hacer con más métodos que simplemente 'copiar' y que se pueden hacer compilaciones complejas. P.ej. un setSpecialOption puede especificar que la instancia de retorno debe ser SpecialConcrete.

Aquí es una ilustración de la solución:

trait Abstract0Builder[To] { 
    def setOption(j: Int) 
    def result: To 
} 

trait CanBuildAbstract0[From, To] { 
    def apply(from: From): Abstract0Builder[To] 
} 


abstract class Abstract0 { 
    type Self <: Abstract0 

    def self = this.asInstanceOf[Self] 

    def setOption[To <: Abstract0](j: Int)(implicit cbf: CanBuildAbstract0[Self, To]): To = { 
    val builder = cbf(self) 
    builder.setOption(j) 
    builder.result 
    } 

} 

class Concrete0(i: Int) extends Abstract0 { 
    type Self = Concrete0 
} 

object Concrete0 { 
    implicit def cbf = new CanBuildAbstract0[Concrete0, Concrete0] { 
     def apply(from: Concrete0) = new Abstract0Builder[Concrete0] { 
      var i = 0 
      def setOption(j: Int) = i = j 
      def result = new Concrete0(i) 
     } 
    } 
} 

object Main { 
    def main(args: Array[String]) { 
    val c = new Concrete0(0).setOption(1) 
    println("c is " + c.getClass) 
    } 
} 

ACTUALIZACIÓN 2: En respuesta a la segunda comentario de JPP. En caso de varios niveles de anidamiento, utilice un parámetro de tipo en lugar de miembro tipo y hacer Abstract0 en un rasgo:

trait Abstract0[+Self <: Abstract0[_]] { 
    // ... 
} 

class Concrete0 extends Abstract0[Concrete0] { 
    // .... 
} 

class RefinedConcrete0 extends Concrete0 with Abstract0[RefinedConcrete0] { 
// .... 
} 
+0

¡Función de lenguaje muy agradable!No recuerdo haberlo visto en el libro Programming in Scala. Pero ahora una pregunta de seguimiento: esto funciona solo al devolver esto. ¿Qué sucede si quiero devolver otro objeto del mismo tipo concreto, por ejemplo, para un método de clonación? (Sé que obtengo un clon gratis como 'copy' en las clases de casos, pero de todos modos me interesaría la pregunta) –

+0

Gracias por la explicación del seguimiento. Una última pregunta. Si 'Concrete0' en sí tiene una subclase' RefinedConcrete0', no puedo anular el tipo concreto Self, ya especificado en 'Concrete0'. ¿Supongo que el patrón de construcción es la opción preferida? ¿O hay algo más que podría usarse en su lugar? Supongo que el ítem 33 de C++ más efectivo también se puede aplicar aquí: Hacer clases de hojas no abstractas ... –

+0

¿Cuál sería la diferencia entre tener 'this.type' y' Abstract0'? No veo ninguno. Para mí esto solo funciona porque de todos modos sigue las reglas de la varianza. – Debilski

4

Este es el caso de uso exacto de this.type. Sería como:

def setOption(...): this.type = { 
    // Do stuff ... 
    this 
} 
Cuestiones relacionadas