405 Stimmen

Was ist der Vorteil von abstrakten Klassen anstelle von Traits?

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.

402voto

Mushtaq Ahmed Punkte 6282

Mir fallen zwei Unterschiede ein

  1. 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
  2. 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

227voto

Eugene Yokota Punkte 92703

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 schreiben super.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 o var 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.

81voto

Daniel C. Sobral Punkte 290004

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.

22voto

Nemanja Boric Punkte 21027

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

11voto

peter p Punkte 778

Bei der Erweiterung einer abstrakten Klasse zeigt dies, dass die Unterklasse von ähnlicher Art ist. Bei der Verwendung von Traits ist dies meiner Meinung nach nicht unbedingt der Fall.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X