Welchen Vorteil hat die Verwendung einer abstrakten Klasse anstelle eines Traits (abgesehen von der Leistung)? Es scheint, dass abstrakte Klassen in den meisten Fällen durch Traits ersetzt werden können.
Antworten
Zu viele Anzeigen?Mir fallen zwei Unterschiede ein
- Abstrakte Klassen können sowohl Konstruktor- als auch Typparameter haben. Traits können nur Typparameter haben. Es gab eine Diskussion darüber, dass in Zukunft auch Traits Konstruktorparameter haben können
- Abstrakte Klassen sind mit Java vollständig interoperabel. Sie können von Java-Code aus ohne jegliche Wrapper aufgerufen werden. Traits sind nur dann vollständig interoperabel, wenn sie keinen Implementierungscode enthalten
Es gibt einen Abschnitt in Programming in Scala namens "Vererben oder nicht vererben?" die sich mit dieser Frage befasst. Da die 1. Auflage online verfügbar ist, hoffe ich, dass es in Ordnung ist, das Ganze hier zu zitieren. (Jeder ernsthafte Scala-Programmierer sollte das Buch kaufen):
Wann immer Sie eine wiederverwendbare Sammlung von Verhaltensweisen implementieren, werden Sie müssen Sie entscheiden, ob Sie einen Trait oder eine abstrakte Klasse verwenden wollen. Es gibt keine feste Regel, aber dieser Abschnitt enthält ein paar Richtlinien, die Sie berücksichtigen.
Wenn das Verhalten nicht wiederverwendet werden soll , dann machen Sie eine konkrete Klasse daraus. Es ist schließlich kein wiederverwendbares Verhalten.
Wenn es in mehreren, nicht verwandten Klassen wiederverwendet werden könnte machen Sie es zu einer Eigenschaft. Nur Traits können in verschiedenen Teilen der Klassenhierarchie gemischt werden.
Wenn Sie in Java-Code davon erben wollen verwenden Sie eine abstrakte Klasse. Da Traits mit Code kein nahes Java-Analogon haben, ist es in der Regel umständlich, von einem Trait in einer Java-Klasse zu erben. Die Vererbung von einer Scala-Klasse ist genau wie die Vererbung von einer Java-Klasse. Eine Ausnahme ist, dass ein Scala-Trait mit nur abstrakten Mitgliedern direkt in eine Java-Schnittstelle, so dass Sie solche Traits auch dann definieren können Traits zu definieren, auch wenn Sie erwarten, dass Java-Code davon erben wird. Siehe Kapitel 29 für weitere Informationen über die Zusammenarbeit mit Java und Scala.
Wenn Sie es in kompilierter Form verbreiten wollen und Sie erwarten draußen Gruppen Klassen schreiben, die von dieser Klasse erben, sollten Sie eher eine eine abstrakte Klasse zu verwenden. Das Problem ist, dass wenn ein Trait ein Mitglied gewinnt oder verliert verlieren, müssen alle Klassen, die davon erben, neu kompiliert werden, auch wenn sie sich nicht geändert haben. Wenn externe Clients nur das Verhalten aufrufen, anstatt davon zu erben, dann ist die Verwendung eines Traits in Ordnung.
Wenn Effizienz sehr wichtig ist neigen dazu, eine Klasse zu verwenden. Die meisten Java Laufzeiten machen den Aufruf einer virtuellen Methode eines Klassenmitglieds zu einer schnelleren als der Aufruf einer Schnittstellenmethode. Traits werden zu Schnittstellen kompiliert und können daher einen leichten Performance-Overhead aufweisen. Sie sollten diese Wahl jedoch nur dann treffen, wenn Sie wissen, dass der betreffende Trait einen Leistungsengpass darstellt und Sie Beweise dafür haben dass die Verwendung einer Klasse stattdessen das Problem tatsächlich löst.
Wenn Sie es noch nicht wissen wenn Sie die oben genannten Punkte berücksichtigt haben, dann beginnen Sie mit mit der Erstellung eines Merkmals. Sie können es später immer noch ändern, und im Allgemeinen hält die Verwendung eines Traits mehr Optionen offen.
Wie @Mushtaq Ahmed erwähnte, kann ein Trait keine Parameter an den primären Konstruktor einer Klasse übergeben.
Ein weiterer Unterschied ist die Behandlung von super
.
Der andere Unterschied zwischen Klassen und Merkmalen besteht darin, dass in Klassen,
super
Aufrufe statisch gebunden sind, sind sie in Traits dynamisch gebunden. Wenn Sie schreibensuper.toString
in einer Klasse, wissen Sie genau, welche Methodenimplementierung aufgerufen wird. Wenn Sie dasselbe in einen Trait schreiben, ist die Methodenimplementierung, die für den Superaufruf aufgerufen werden soll, bei der Definition des Traits jedoch nicht definiert.
Siehe den Rest von Kapitel 12 für weitere Einzelheiten.
Bearbeiten 1 (2013):
Es gibt einen subtilen Unterschied im Verhalten von abstrakten Klassen im Vergleich zu Traits. Eine der Linearisierungsregeln besteht darin, dass die Vererbungshierarchie der Klassen beibehalten wird, was dazu führt, dass abstrakte Klassen in der Kette nach hinten verschoben werden, während Traits ohne weiteres hineingemischt werden können. Unter bestimmten Umständen ist es sogar besser, sich in der letzten Position der Klassenlinearisierung zu befinden, so dass abstrakte Klassen dafür verwendet werden können. Siehe Einschränkung der Klassenlinearisierung (Mixin-Reihenfolge) in Scala .
Edit 2 (2018):
Seit Scala 2.12 hat sich das Binärkompatibilitätsverhalten von Trait geändert. Vor 2.12 erforderte das Hinzufügen oder Entfernen eines Mitglieds zum Trait die Neukompilierung aller Klassen, die den Trait erben, selbst wenn sich die Klassen nicht geändert haben. Dies ist auf die Art und Weise zurückzuführen, wie Traits in der JVM kodiert wurden.
Ab Scala 2.12 werden Traits zu Java-Schnittstellen kompilieren Die Anforderungen haben sich also ein wenig gelockert. Wenn der Trait eine der folgenden Funktionen hat, müssen seine Unterklassen immer noch neu kompiliert werden:
- Felder definieren (
val
ovar
aber eine Konstante ist in Ordnung -final val
ohne Ergebnistyp)- aufrufen
super
- Initialisierungsanweisungen im Textkörper
- eine Klasse erweitern
- sich auf die Linearisierung verlassen, um Implementierungen im richtigen Supertrait zu finden
Ist dies jedoch nicht der Fall, können Sie ihn jetzt aktualisieren, ohne die Binärkompatibilität zu verletzen.
Was auch immer es wert ist, die Studie von Odersky et al. Programmieren in Scala empfiehlt, im Zweifelsfall auf Merkmale zurückzugreifen. Sie können sie später bei Bedarf immer noch in abstrakte Klassen umwandeln.
Abgesehen von der Tatsache, dass man nicht direkt mehrere abstrakte Klassen erweitern kann, sondern mehrere Traits in eine Klasse mixen kann, ist es erwähnenswert, dass Traits stapelbar sind, da Superaufrufe in einem Trait dynamisch gebunden sind (sie beziehen sich auf eine Klasse oder einen Trait, der vor dem aktuellen gemischt wurde).
Aus der Antwort von Thomas in Unterschied zwischen abstrakter Klasse und Trait :
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
- See previous answers
- Weitere Antworten anzeigen