Quel est l'avantage d'utiliser des classes abstraites au lieu de traits?

371

Quel est l'avantage d'utiliser une classe abstraite au lieu d'un trait (en dehors de la performance)? Il semble que les classes abstraites puissent être remplacées par des traits dans la plupart des cas.

Ralf
la source

Réponses:

372

Je peux penser à deux différences

  1. Les classes abstraites peuvent avoir des paramètres de constructeur ainsi que des paramètres de type. Les traits ne peuvent avoir que des paramètres de type. Il y a eu une discussion sur le fait qu'à l'avenir, même les traits peuvent avoir des paramètres constructeurs
  2. Les classes abstraites sont entièrement interopérables avec Java. Vous pouvez les appeler à partir du code Java sans wrappers. Les traits ne sont pleinement interopérables que s'ils ne contiennent aucun code d'implémentation
Mushtaq Ahmed
la source
173
Addendum très important: une classe peut hériter de plusieurs traits mais d'une seule classe abstraite. Je pense que cela devrait être la première question qu'un développeur pose lorsqu'il envisage laquelle utiliser dans presque tous les cas.
BAR du
16
bouée de sauvetage: "Les traits ne sont pleinement interopérables que s'ils ne contiennent aucun code d'implémentation"
Walrus the Cat
2
abstrait - lorsque les comportements collectifs définissent ou mènent à un objet (branche d'objet) mais toujours pas encore composé pour être comme objet (prêt). Traits, lorsque vous devez intégrer des capacités, c'est-à-dire que les capacités ne proviennent jamais de la création d'un objet, elles évoluent ou sont nécessaires lorsqu'un objet sort de l'isolement et doit communiquer.
Ramiz Uddin du
5
La deuxième différence n'existe pas en Java8, pensez.
Duong Nguyen
14
Selon Scala 2.12, un trait se compile sur une interface Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith
209

Il y a une section dans Programmation dans Scala appelée "Pour trait, ou pas pour trait?" qui répond à cette question. Puisque la 1ère édition est disponible en ligne, j'espère que c'est bien de citer le tout ici. (Tout programmeur Scala sérieux devrait acheter le livre):

Chaque fois que vous implémentez une collection de comportements réutilisable, vous devrez décider si vous souhaitez utiliser un trait ou une classe abstraite. Il n'y a pas de règle ferme, mais cette section contient quelques directives à considérer.

Si le comportement ne sera pas réutilisé , faites-en une classe concrète. Ce n'est pas un comportement réutilisable après tout.

S'il peut être réutilisé dans plusieurs classes indépendantes , faites-en un trait. Seuls les traits peuvent être mélangés dans différentes parties de la hiérarchie des classes.

Si vous souhaitez en hériter en code Java , utilisez une classe abstraite. Étant donné que les traits avec du code n'ont pas d'analogue Java proche, il a tendance à être gênant d'hériter d'un trait dans une classe Java. L'héritage d'une classe Scala, quant à lui, est exactement comme l'héritage d'une classe Java. À titre d'exception, un trait Scala avec uniquement des membres abstraits se traduit directement par une interface Java, vous devriez donc vous sentir libre de définir de tels traits même si vous vous attendez à ce que le code Java en hérite. Voir le chapitre 29 pour plus d'informations sur l'utilisation conjointe de Java et Scala.

Si vous prévoyez de le distribuer sous forme compilée et que vous vous attendez à ce que des groupes externes écrivent des classes qui en héritent, vous pourriez vous pencher vers l'utilisation d'une classe abstraite. Le problème est que lorsqu'un trait gagne ou perd un membre, toutes les classes qui en héritent doivent être recompilées, même si elles n'ont pas changé. Si des clients externes n'appellent que le comportement, au lieu d'en hériter, alors utiliser un trait est très bien.

Si l'efficacité est très importante , penchez-vous vers l'utilisation d'une classe. La plupart des runtimes Java font une invocation de méthode virtuelle d'un membre de classe une opération plus rapide qu'une invocation de méthode d'interface. Les traits sont compilés sur les interfaces et peuvent donc payer une légère surcharge de performances. Cependant, vous ne devez faire ce choix que si vous savez que le trait en question constitue un goulot d'étranglement des performances et que vous avez la preuve que l'utilisation d'une classe à la place résout réellement le problème.

Si vous ne savez toujours pas , après avoir considéré ce qui précède, commencez par en faire un trait. Vous pouvez toujours le modifier plus tard, et en général, l'utilisation d'un trait laisse plus d'options ouvertes.

Comme l'a mentionné @Mushtaq Ahmed, un trait ne peut avoir aucun paramètre transmis au constructeur principal d'une classe.

Une autre différence est le traitement des super.

L'autre différence entre les classes et les traits est que, alors que dans les classes, les superappels sont liés statiquement, dans les traits, ils sont liés dynamiquement. Si vous écrivez super.toStringdans une classe, vous savez exactement quelle implémentation de méthode sera invoquée. Cependant, lorsque vous écrivez la même chose dans un trait, l'implémentation de la méthode à appeler pour le super appel n'est pas définie lorsque vous définissez le trait.

Voir le reste du chapitre 12 pour plus de détails.

Édition 1 (2013):

Il existe une différence subtile dans le comportement des classes abstraites par rapport aux traits. L'une des règles de linéarisation est qu'elle préserve la hiérarchie d'héritage des classes, ce qui tend à pousser les classes abstraites plus tard dans la chaîne tandis que les traits peuvent être mélangés avec bonheur. Dans certaines circonstances, il est en fait préférable d'être dans la dernière position de la linéarisation de classe , donc des classes abstraites pourraient être utilisées pour cela. Voir contrainte de linéarisation de classe (ordre de mixage) dans Scala .

Édition 2 (2018):

Depuis Scala 2.12, le comportement de compatibilité binaire de trait a changé. Avant la 2.12, l'ajout ou la suppression d'un membre au trait nécessitait une recompilation de toutes les classes héritant du trait, même si les classes n'avaient pas changé. Cela est dû à la façon dont les traits ont été encodés dans JVM.

Depuis Scala 2.12, les traits se compilent vers les interfaces Java , donc l'exigence s'est un peu relâchée. Si le trait fait l'une des actions suivantes, ses sous-classes nécessitent toujours une recompilation:

  • définir des champs ( valou var, mais une constante est correcte - final valsans type de résultat)
  • appel super
  • instructions d'initialisation dans le corps
  • extension d'une classe
  • s'appuyant sur la linéarisation pour trouver des implémentations dans le supertrait de droite

Mais si le trait ne fonctionne pas, vous pouvez maintenant le mettre à jour sans rompre la compatibilité binaire.

Eugene Yokota
la source
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Quelqu'un pourrait-il expliquer quelle est la différence ici? extendsvs with?
0fnt le
2
@ 0fnt Sa distinction ne concerne pas les extensions vs. Ce qu'il dit, c'est que si vous ne mélangez que le trait dans la même compilation, les problèmes de compatibilité binaire ne s'appliquent pas. Cependant, si votre API est conçue pour permettre aux utilisateurs de mélanger eux-mêmes le trait, vous devrez vous soucier de la compatibilité binaire.
John Colanduoni
2
@ 0fnt: Il n'y a absolument aucune différence sémantique entre extendset with. C'est purement syntaxique. Si vous héritez de plusieurs modèles, le premier obtient extend, tous les autres obtiennent with, c'est tout. Pensez withcomme une virgule: class Foo extends Bar, Baz, Qux.
Jörg W Mittag
77

Pour tout ce que cela vaut, la programmation d'Odersky et al dans Scala recommande que, en cas de doute, vous utilisez des traits. Vous pouvez toujours les changer en classes abstraites plus tard si nécessaire.

Daniel C. Sobral
la source
20

Outre le fait que vous ne pouvez pas étendre directement plusieurs classes abstraites, mais que vous pouvez mélanger plusieurs traits dans une classe, il convient de mentionner que les traits sont empilables, car les super-appels dans un trait sont liés dynamiquement (il s'agit d'une classe ou d'un trait mélangé avant l'actuel).

Extrait de la réponse de Thomas dans Différence entre classe abstraite et caractère :

trait A{
    def a = 1
}

trait X extends A{
    override def a = {
        println("X")
        super.a
    }
}  


trait Y extends A{
    override def a = {
        println("Y")
        super.a
    }
}

scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1

scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1
Nemanja Boric
la source
9

Lors de l'extension d'une classe abstraite, cela montre que la sous-classe est d'un type similaire. Ce n'est pas nécessairement le cas lors de l'utilisation de traits, je pense.

peter p
la source
Cela a-t-il des implications pratiques ou rend-il seulement le code plus facile à comprendre?
Ralf
8

Dans Programming Scala, les auteurs disent que les classes abstraites établissent une relation classique "est-un" orientée objet tandis que les traits sont une voie de composition scala.

Martre
la source
5

Les classes abstraites peuvent contenir un comportement - Elles peuvent être paramétrées avec des arguments de constructeur (quels traits ne peuvent pas) et représenter une entité fonctionnelle. Les traits représentent plutôt une seule fonctionnalité, une interface d'une fonctionnalité.

Dario
la source
8
J'espère que vous n'impliquez pas que les traits ne peuvent pas contenir de comportement. Les deux peuvent contenir du code d'implémentation.
Mitch Blevins
1
@Mitch Blevins: Bien sûr que non. Ils peuvent contenir du code, mais lorsque vous définissez trait Enumerableavec beaucoup de fonctions d'assistance, je ne les appellerais pas comportement mais simplement des fonctionnalités liées à une seule fonctionnalité.
Dario
4
@Dario Je vois "comportement" et "fonctionnalité" comme des synonymes, donc je trouve votre réponse très déroutante.
David J.1
3
  1. Une classe peut hériter de plusieurs traits mais d'une seule classe abstraite.
  2. Les classes abstraites peuvent avoir des paramètres de constructeur ainsi que des paramètres de type. Les traits ne peuvent avoir que des paramètres de type. Par exemple, vous ne pouvez pas dire le trait t (i: Int) {}; le paramètre i est illégal.
  3. Les classes abstraites sont entièrement interopérables avec Java. Vous pouvez les appeler à partir du code Java sans wrappers. Les traits ne sont pleinement interopérables que s'ils ne contiennent aucun code d'implémentation.
pavan.vn101
la source