Was ist der Vorteil der Verwendung abstrakter Klassen anstelle von Merkmalen?

371

Was ist der Vorteil der Verwendung einer abstrakten Klasse anstelle eines Merkmals (abgesehen von der Leistung)? Es scheint, dass abstrakte Klassen in den meisten Fällen durch Merkmale ersetzt werden können.

Ralf
quelle

Antworten:

371

Ich kann mir zwei Unterschiede vorstellen

  1. Abstrakte Klassen können sowohl Konstruktorparameter als auch Typparameter haben. Merkmale können nur Typparameter haben. Es gab einige Diskussionen darüber, dass in Zukunft sogar Merkmale Konstruktorparameter haben können
  2. Abstrakte Klassen sind vollständig mit Java kompatibel. Sie können sie aus Java-Code ohne Wrapper aufrufen. Merkmale sind nur dann vollständig interoperabel, wenn sie keinen Implementierungscode enthalten
Mushtaq Ahmed
quelle
172
Sehr wichtiger Nachtrag: Eine Klasse kann von mehreren Merkmalen erben, aber nur von einer abstrakten Klasse. Ich denke, dies sollte die erste Frage sein, die ein Entwickler stellt, wenn er überlegt, welche er in fast allen Fällen verwenden soll.
BAR
15
Lebensretter: "Merkmale sind nur dann vollständig interoperabel, wenn sie keinen Implementierungscode enthalten"
Walross the Cat
2
abstrakt - wenn kollektives Verhalten ein Objekt (Zweig des Objekts) definiert oder zu diesem führt, aber noch nicht als (fertiges) Objekt zusammengesetzt ist. Merkmale: Wenn Sie Fähigkeiten einführen müssen, dh Fähigkeiten, die niemals aus der Erstellung eines Objekts stammen, werden sie entwickelt oder benötigt, wenn ein Objekt aus der Isolation herauskommt und kommunizieren muss.
Ramiz Uddin
5
Der zweite Unterschied besteht in Java8 nicht.
Duong Nguyen
14
Gemäß Scala 2.12 wird ein Merkmal zu einer Java 8-Schnittstelle kompiliert - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith
209

Es gibt einen Abschnitt in der Programmierung in Scala mit dem Titel "Zu Merkmalen oder nicht zu Merkmalen?" welches diese Frage anspricht. Da die 1. Ausgabe 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 Verhalten implementieren, müssen Sie entscheiden, ob Sie ein Merkmal oder eine abstrakte Klasse verwenden möchten. Es gibt keine feste Regel, aber dieser Abschnitt enthält einige zu berücksichtigende Richtlinien.

Wenn das Verhalten nicht wiederverwendet wird , machen Sie es zu einer konkreten Klasse. Es ist schließlich kein wiederverwendbares Verhalten.

Wenn es in mehreren nicht verwandten Klassen wiederverwendet werden kann , machen Sie es zu einem Merkmal. Nur Merkmale können in verschiedene Teile der Klassenhierarchie gemischt werden.

Wenn Sie in Java-Code davon erben möchten , verwenden Sie eine abstrakte Klasse. Da Merkmale mit Code kein enges Java-Analogon haben, ist es in der Regel umständlich, von einem Merkmal in einer Java-Klasse zu erben. Das Erben von einer Scala-Klasse ist genau wie das Erben von einer Java-Klasse. Als eine Ausnahme wird ein Scala-Merkmal mit nur abstrakten Mitgliedern direkt in eine Java-Schnittstelle übersetzt. Sie können diese Merkmale also auch dann definieren, wenn Sie erwarten, dass Java-Code davon erbt. Weitere Informationen zur Zusammenarbeit mit Java und Scala finden Sie in Kapitel 29.

Wenn Sie es in kompilierter Form verteilen möchten , und Sie erwarten, dass externe Gruppen Klassen schreiben, die davon erben, können Sie eine abstrakte Klasse verwenden. Das Problem ist, dass, wenn ein Merkmal ein Mitglied gewinnt oder verliert, alle Klassen, die von ihm erben, neu kompiliert werden müssen, auch wenn sie sich nicht geändert haben. Wenn externe Clients nur das Verhalten aufrufen, anstatt von ihm zu erben, ist die Verwendung eines Merkmals in Ordnung.

Wenn Effizienz sehr wichtig ist , tendieren Sie zur Verwendung einer Klasse. Die meisten Java-Laufzeiten machen einen Aufruf einer virtuellen Methode eines Klassenmitglieds zu einer schnelleren Operation als einen Aufruf einer Schnittstellenmethode. Merkmale werden zu Schnittstellen kompiliert und können daher einen geringen Leistungsaufwand verursachen. Sie sollten diese Auswahl jedoch nur treffen, wenn Sie wissen, dass das betreffende Merkmal einen Leistungsengpass darstellt und nachweisen können, dass die Verwendung einer Klasse das Problem tatsächlich löst.

Wenn Sie es nach Berücksichtigung der obigen Punkte immer noch nicht wissen , machen Sie es zunächst als Merkmal. Sie können es später jederzeit ändern, und im Allgemeinen bleiben durch die Verwendung eines Merkmals mehr Optionen offen.

Wie @Mushtaq Ahmed erwähnte, können für ein Merkmal keine Parameter an den primären Konstruktor einer Klasse übergeben werden.

Ein weiterer Unterschied ist die Behandlung von super.

Der andere Unterschied zwischen Klassen und Merkmalen besteht darin, dass superAnrufe in Klassen statisch gebunden sind , während sie in Merkmalen dynamisch gebunden sind. Wenn Sie super.toStringin eine Klasse schreiben , wissen Sie genau, welche Methodenimplementierung aufgerufen wird. Wenn Sie dasselbe in ein Merkmal schreiben, ist die für den Superaufruf aufzurufende Methodenimplementierung jedoch nicht definiert, wenn Sie das Merkmal definieren.

Weitere Informationen finden Sie im Rest von Kapitel 12 .

Edit 1 (2013):

Es gibt einen subtilen Unterschied im Verhalten abstrakter Klassen im Vergleich zu Merkmalen. Eine der Linearisierungsregeln besteht darin, dass die Vererbungshierarchie der Klassen beibehalten wird, wodurch abstrakte Klassen später in der Kette verschoben werden, während Merkmale problemlos eingemischt werden können. Unter bestimmten Umständen ist es tatsächlich vorzuziehen, sich an der letzten Position der Klassenlinearisierung zu befinden Daher könnten abstrakte Klassen dafür verwendet werden. Siehe Einschränken der Klassenlinearisierung (Mixin-Reihenfolge) in Scala .

Edit 2 (2018):

Ab Scala 2.12 hat sich das binäre Kompatibilitätsverhalten von Merkmalen geändert. Vor 2.12 mussten zum Hinzufügen oder Entfernen eines Mitglieds zum Merkmal alle Klassen neu kompiliert werden, die das Merkmal erben, auch wenn sich die Klassen nicht geändert haben. Dies liegt an der Art und Weise, wie Merkmale in JVM codiert wurden.

Ab Scala 2.12 werden Merkmale in Java-Schnittstellen kompiliert , sodass sich die Anforderungen etwas entspannt haben. Wenn das Merkmal eine der folgenden Aktionen ausführt, müssen seine Unterklassen immer noch neu kompiliert werden:

  • Felder definieren ( valoder var, aber eine Konstante ist in Ordnung - final valohne Ergebnistyp)
  • Berufung super
  • Initialisiereranweisungen im Körper
  • eine Klasse erweitern
  • Verlassen Sie sich auf die Linearisierung, um Implementierungen im richtigen Supertrait zu finden

Wenn dies nicht der Fall ist, können Sie es jetzt aktualisieren, ohne die Binärkompatibilität zu beeinträchtigen.

Eugene Yokota
quelle
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Könnte jemand erklären, was hier der Unterschied ist? extendsvs with?
0fnt
2
@ 0fnt Bei seiner Unterscheidung geht es nicht darum, sich gegen zu erweitern. Was er sagt ist, dass, wenn Sie nur das Merkmal innerhalb derselben Zusammenstellung mischen, die Probleme mit der Binärkompatibilität nicht zutreffen. Wenn Ihre API jedoch so konzipiert ist, dass Benutzer das Merkmal selbst einmischen können, müssen Sie sich um die Binärkompatibilität sorgen.
John Colanduoni
2
@ 0fnt: Es gibt absolut keinen semantischen Unterschied zwischen extendsund with. Es ist rein syntaktisch. Wenn Sie von mehreren Vorlagen erben, wird die erste extend, alle anderen erhalten with, das war's. Stellen Sie sich withein Komma vor : class Foo extends Bar, Baz, Qux.
Jörg W Mittag
77

Für alles, was es wert ist, empfiehlt Odersky et al. In der Programmierung in Scala , dass Sie im Zweifelsfall Merkmale verwenden. Sie können sie später bei Bedarf jederzeit in abstrakte Klassen ändern.

Daniel C. Sobral
quelle
20

Abgesehen von der Tatsache, dass Sie nicht mehrere abstrakte Klassen direkt erweitern können, sondern mehrere Merkmale zu einer Klasse mischen können, ist es erwähnenswert, dass Merkmale stapelbar sind, da Superaufrufe in einem Merkmal dynamisch gebunden sind (es bezieht sich auf eine Klasse oder ein Merkmal, das zuvor gemischt wurde der Momentane).

Aus Thomas 'Antwort im Unterschied zwischen abstrakter Klasse und Merkmal :

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
quelle
9

Wenn Sie eine abstrakte Klasse erweitern, zeigt dies, dass die Unterklasse von ähnlicher Art ist. Dies ist meiner Meinung nach bei der Verwendung von Merkmalen nicht unbedingt der Fall.

peter p
quelle
Hat dies praktische Auswirkungen oder macht es den Code nur leichter verständlich?
Ralf
8

In Programming Scala sagen die Autoren, dass abstrakte Klassen eine klassische objektorientierte "is-a" -Beziehung eingehen, während Merkmale eine Scala-Art der Komposition sind.

Marder
quelle
5

Abstrakte Klassen können Verhalten enthalten - Sie können mit Konstruktorargumenten parametrisiert werden (welche Merkmale nicht können) und eine Arbeitseinheit darstellen. Merkmale repräsentieren stattdessen nur ein einzelnes Merkmal, eine Schnittstelle einer Funktionalität.

Dario
quelle
8
Ich hoffe, Sie implizieren nicht, dass Merkmale kein Verhalten enthalten können. Beide können Implementierungscode enthalten.
Mitch Blevins
1
@Mitch Blevins: Natürlich nicht. Sie können Code enthalten, aber wenn Sie trait Enumerablemit vielen Hilfsfunktionen definieren , würde ich sie nicht als Verhalten bezeichnen, sondern nur als Funktionen, die mit einer Funktion verbunden sind.
Dario
4
@Dario Ich sehe "Verhalten" und "Funktionalität" als Synonyme, daher finde ich Ihre Antwort sehr verwirrend.
David J.
3
  1. Eine Klasse kann von mehreren Merkmalen erben, aber nur von einer abstrakten Klasse.
  2. Abstrakte Klassen können sowohl Konstruktorparameter als auch Typparameter haben. Merkmale können nur Typparameter haben. Zum Beispiel können Sie nicht Merkmal t (i: Int) {} sagen; Der Parameter i ist unzulässig.
  3. Abstrakte Klassen sind vollständig mit Java kompatibel. Sie können sie aus Java-Code ohne Wrapper aufrufen. Merkmale sind nur dann vollständig interoperabel, wenn sie keinen Implementierungscode enthalten.
pavan.vn101
quelle