¿Cuál es la diferencia entre autotipos y subclases de rasgos?
Un autotipo para un rasgo A
:
trait B
trait A { this: B => }
Dice que " A
no se puede mezclar en una clase concreta que no se extienda también B
" .
Por otro lado, lo siguiente:
trait B
trait A extends B
dice que "cualquier clase (concreta o abstracta) que se mezcle A
también se mezclará en B" .
¿No significan lo mismo estas dos afirmaciones? El tipo propio parece servir sólo para crear la posibilidad de un simple error en tiempo de compilación.
¿Qué me estoy perdiendo?
Se utiliza predominantemente para la inyección de dependencias , como en el patrón Cake. Existe un excelente artículo que cubre muchas formas diferentes de inyección de dependencia en Scala, incluido Cake Pattern. Si busca en Google "Cake Pattern and Scala", obtendrá muchos enlaces, incluidas presentaciones y videos. Por ahora, aquí hay un enlace a otra pregunta .
Ahora bien, en cuanto a cuál es la diferencia entre un tipo de yo y extender un rasgo, eso es simple. Si dices B extends A
, entonces B
es un A
. Cuando usas tipos propios, B
requiere un archivo A
. Hay dos requisitos específicos que se crean con los tipos propios:
- Si
B
está extendido, entonces deberás mezclar un archivoA
. - Cuando una clase concreta finalmente extiende/mezcla estos rasgos, alguna clase/rasgo debe implementarse
A
.
Considere los siguientes ejemplos:
scala> trait User { def name: String }
defined trait User
scala> trait Tweeter {
| user: User =>
| def tweet(msg: String) = println(s"$name: $msg")
| }
defined trait Tweeter
scala> trait Wrong extends Tweeter {
| def noCanDo = name
| }
<console>:9: error: illegal inheritance;
self-type Wrong does not conform to Tweeter's selftype Tweeter with User
trait Wrong extends Tweeter {
^
<console>:10: error: not found: value name
def noCanDo = name
^
Si Tweeter
fuera una subclase de User
, no habría ningún error. En el código anterior, requerimos que se use User
siempre , sin embargo, no se proporcionó , por lo que recibimos un error. Ahora, con el código anterior todavía dentro del alcance, considere:Tweeter
User
Wrong
scala> trait DummyUser extends User {
| override def name: String = "foo"
| }
defined trait DummyUser
scala> trait Right extends Tweeter with User {
| val canDo = name
| }
defined trait Right
scala> trait RightAgain extends Tweeter with DummyUser {
| val canDo = name
| }
defined trait RightAgain
Con , se cumple Right
el requisito de mezclar a . User
Sin embargo, el segundo requisito mencionado anteriormente no se cumple: la carga de la implementación User
aún permanece para las clases/rasgos que se extienden Right
.
Con RightAgain
ambos requisitos se cumplen. Se proporcionan A User
y una implementación de .User
Para casos de uso más prácticos, consulte los enlaces al comienzo de esta respuesta. Pero espero que ahora lo entiendas.
Los tipos propios le permiten definir dependencias cíclicas. Por ejemplo, puedes lograr esto:
trait A { self: B => }
trait B { self: A => }
El uso de herencia extends
no lo permite. Intentar:
trait A extends B
trait B extends A
error: illegal cyclic reference involving trait A
En el libro de Odersky, consulte la sección 33.5 (capítulo Creación de interfaz de usuario de hoja de cálculo) donde menciona:
En el ejemplo de la hoja de cálculo, la clase Modelo hereda de Evaluador y, por lo tanto, obtiene acceso a su método de evaluación. Por el contrario, la clase Evaluador define su tipo propio como Modelo, así:
package org.stairwaybook.scells
trait Evaluator { this: Model => ...
Una diferencia adicional es que los tipos propios pueden especificar tipos que no son de clase. Por ejemplo
trait Foo{
this: { def close:Unit} =>
...
}
El tipo propio aquí es un tipo estructural. El efecto es decir que cualquier cosa que se mezcle en Foo debe implementar una unidad de devolución de método "cerrado" sin argumentos. Esto permite crear mixins seguros para escribir patos.
Otra cosa que no se ha mencionado: debido a que los tipos propios no son parte de la jerarquía de la clase requerida, pueden excluirse de la comparación de patrones, especialmente cuando se compara exhaustivamente con una jerarquía sellada. Esto resulta útil cuando se desea modelar comportamientos ortogonales como:
sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition
val p : Person = new Student {}
p match {
case s : Student => println("a student")
case t : Teacher => println("a teacher")
} // that's it we're exhaustive