Un self-type pour un trait A
:
trait B
trait A { this: B => }
dit que "A
ne peut pas être mélangé dans une classe concrète qui ne s'étend pas également B
".
Par contre, ce qui suit:
trait B
trait A extends B
dit que "toute classe (concrète ou abstraite) mélangée dans A
sera également mélangée dans B".
Ces deux déclarations ne signifient-elles pas la même chose? Le self-type semble ne servir qu'à créer la possibilité d'une simple erreur de compilation.
Qu'est-ce que je rate?
Il est principalement utilisé pour Injection de dépendance , comme dans le modèle de gâteau. Il existe un excellent article couvrant de nombreuses formes différentes d'injection de dépendance dans Scala, y compris le modèle de gâteau. Si vous utilisez Google "Modèle de gâteau et Scala", vous obtiendrez de nombreux liens, notamment des présentations et des vidéos. Pour l'instant, voici un lien vers ne autre question .
Maintenant, quant à la différence entre un type de soi et l’extension d’un trait, c’est simple. Si vous dites B extends A
, alors B
est un A
. Lorsque vous utilisez des self-types, B
nécessite un A
. Deux exigences spécifiques sont créées avec des types individuels:
B
est étendu, il vous faut pour mélanger un A
.A
.Considérez les exemples suivants:
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
était une sous-classe de User
, il n'y aurait pas d'erreur. Dans le code ci-dessus, nous demandions un User
à chaque fois que Tweeter
est utilisé, mais un User
n'était pas fourni à Wrong
, nous avons donc une erreur. Maintenant, avec le code ci-dessus toujours dans la portée, considérons:
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
Avec Right
, il est satisfait à la nécessité de mélanger un User
. Cependant, la deuxième condition mentionnée ci-dessus n'est pas satisfaite: la charge de la mise en œuvre de User
subsiste pour les classes/traits qui s'étendent Right
.
Avec RightAgain
les deux conditions sont remplies. Un User
et une implémentation de User
sont fournis.
Pour des cas d'utilisation plus pratiques, veuillez consulter les liens au début de cette réponse! Mais si tout va bien maintenant vous l'obtenez.
Les types d'individus vous permettent de définir des dépendances cycliques. Par exemple, vous pouvez réaliser ceci:
trait A { self: B => }
trait B { self: A => }
L'héritage avec extends
ne le permet pas. Essayer:
trait A extends B
trait B extends A
error: illegal cyclic reference involving trait A
Dans le livre Odersky, reportez-vous à la section 33.5 (Création d’un chapitre sur l’interface utilisateur de feuille de calcul) où elle est mentionnée:
Dans l'exemple de feuille de calcul, la classe Model hérite de Evaluator et accède ainsi à sa méthode d'évaluation. Pour l'inverse, la classe Evaluator définit son type comme étant Model, comme ceci:
package org.stairwaybook.scells
trait Evaluator { this: Model => ...
J'espère que cela t'aides.
Une différence supplémentaire est que les self-types peuvent spécifier des types sans classe. Par exemple
trait Foo{
this: { def close:Unit} =>
...
}
Le type de self ici est un type structurel. L'effet est de dire que tout ce qui se mélange dans Foo doit implémenter une unité renvoyant la méthode "sans argument". Cela permet des mélanges sécurisés pour le typage de canards.
La section 2.3 "Annotations sur types distincts" du document original de Scala de Martin Odersky Abstractions de composants évolutifs explique en fait le but du type autonome bien au-delà de la composition en mix: offre un moyen alternatif d’associer une classe à un type abstrait.
L'exemple donné dans l'article était le suivant et il ne semble pas avoir de correspondant élégant dans la sous-classe:
abstract class Graph {
type Node <: BaseNode;
class BaseNode {
self: Node =>
def connectWith(n: Node): Edge =
new Edge(self, n);
}
class Edge(from: Node, to: Node) {
def source() = from;
def target() = to;
}
}
class LabeledGraph extends Graph {
class Node(label: String) extends BaseNode {
def getLabel: String = label;
def self: Node = this;
}
}
Une autre chose qui n’a pas été mentionnée: les self-types ne faisant pas partie de la hiérarchie de la classe requise, ils peuvent être exclus de la correspondance de modèle, en particulier lorsque vous effectuez une correspondance exhaustive avec une hiérarchie scellée. Ceci est pratique lorsque vous souhaitez modéliser des comportements orthogonaux tels que:
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
TL; DR résumé des autres réponses:
Les types que vous étendez sont exposés aux types hérités, mais les types propres ne le sont pas.
exemple: class Cow { this: FourStomachs }
vous permet d'utiliser des méthodes uniquement disponibles pour les ruminants, telles que digestGrass
. Les traits qui prolongent Vache n'auront cependant pas de tels privilèges. D'autre part, class Cow extends FourStomachs
exposera digestGrass
à toute personne qui extends Cow
.
les types permettent des dépendances cycliques, l'extension d'autres types ne
Commençons par la dépendance cyclique.
trait A {
selfA: B =>
def fa: Int }
trait B {
selfB: A =>
def fb: String }
Toutefois, la modularité de cette solution n’est pas aussi grande qu’elle pourrait l’être au premier abord, car vous pouvez remplacer les types de self de la manière suivante:
trait A1 extends A {
selfA1: B =>
override def fb = "B's String" }
trait B1 extends B {
selfB1: A =>
override def fa = "A's String" }
val myObj = new A1 with B1
Toutefois, si vous substituez un membre d'un type self, vous perdez l'accès au membre d'origine, auquel il est toujours possible d'accéder via l'héritage super. Donc, ce qui est vraiment gagné en utilisant l'héritage, c'est:
trait AB {
def fa: String
def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }
trait B1 extends AB
{ override def fb = "B's String" }
val myObj = new A1 with B1
Maintenant, je ne peux pas prétendre comprendre toutes les subtilités du modèle de gâteau, mais il me semble que la méthode principale pour appliquer la modularité consiste à utiliser la composition plutôt que les types d'héritage ou de soi.
La version d'héritage est plus courte, mais la principale raison pour laquelle je préfère l'héritage aux types d'individus est que je trouve qu'il est beaucoup plus difficile d'obtenir l'ordre d'initialisation correct avec les types d'individus. Cependant, vous pouvez faire certaines choses avec les types de soi que vous ne pouvez pas faire avec l'héritage. Les types auto peuvent utiliser un type alors que l'héritage nécessite un trait ou une classe comme dans:
trait Outer
{ type T1 }
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.
Vous pouvez même faire:
trait TypeBuster
{ this: Int with String => }
Bien que vous ne puissiez jamais l'instancier. Je ne vois aucune raison absolue de ne pas pouvoir hériter d'un type, mais j'estime qu'il serait utile de disposer de classes et de traits de constructeur de chemins, tout comme nous avons des traits/classes de constructeur de types. Comme malheureusement
trait InnerA extends Outer#Inner //Doesn't compile
Nous avons ceci:
trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }
Ou ca:
trait Outer
{ trait Inner }
trait InnerA
{this: Outer#Inner =>}
trait InnerB
{this: Outer#Inner =>}
trait OuterFinal extends Outer
{ val myVal = new InnerA with InnerB with Inner }
Un point qui devrait être plus empathisé est que les traits peuvent prolonger les classes. Merci à David Maclver pour l'avoir signalé. Voici un exemple de mon propre code:
class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
ScnBase
hérite de la classe Swing Frame, elle pourrait donc être utilisée comme type self, puis être mélangée à la fin (lors de l'instanciation). Cependant, val geomR
doit être initialisé avant d'être utilisé pour hériter des traits. Nous avons donc besoin d’une classe pour forcer l’initialisation préalable de geomR
. La classe ScnVista
peut ensuite être héritée de plusieurs traits orthogonaux dont ils peuvent eux-mêmes hériter. L'utilisation de plusieurs paramètres de type (génériques) offre une autre forme de modularité.
trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}
// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10
// 2.
trait X {
type SomeA <: A
trait Inner1 { this: SomeA => } // compiles ok
trait Inner2 extends SomeA {} // doesn't compile
}
Un type auto vous permet de spécifier les types autorisés à mélanger un trait. Par exemple, si vous avez un trait avec un type de soi Closeable
, alors ce trait sait que les seules choses autorisées à le mélanger, doivent implémenter l'interface Closeable
.
Mise à jour: Une différence principale est que les self-types peuvent dépendre de plusieurs classes (j'admets que c'est un peu le cas). Par exemple, vous pouvez avoir
class Person {
//...
def name: String = "...";
}
class Expense {
def cost: Int = 123;
}
trait Employee {
this: Person with Expense =>
// ...
def roomNo: Int;
def officeLabel: String = name + "/" + roomNo;
}
Cela permet d’ajouter le Employee
mixin à tout ce qui est une sous-classe de Person
et Expense
. Bien entendu, cela n'a de sens que si Expense
s'étend Person
ou inversement. Le fait est que l'utilisation de self-types Employee
peut être indépendante de la hiérarchie des classes dont elle dépend. Peu importe ce qui s'étend - Si vous changez la hiérarchie de Expense
vs Person
, vous n'avez pas à modifier Employee
.
dans le premier cas, un sous-trait ou une sous-classe de B peut être mélangé à n'importe quel usage de A. Ainsi, B peut être un trait abstrait.