Antworten:
Ich kann mir zwei Unterschiede vorstellen
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
super
Anrufe in Klassen statisch gebunden sind , während sie in Merkmalen dynamisch gebunden sind. Wenn Siesuper.toString
in 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 (
val
odervar
, aber eine Konstante ist in Ordnung -final val
ohne 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.
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? extends
vs with
?
extends
und 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 with
ein Komma vor : class Foo extends Bar, Baz, Qux
.
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.
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
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.
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.
trait Enumerable
mit vielen Hilfsfunktionen definieren , würde ich sie nicht als Verhalten bezeichnen, sondern nur als Funktionen, die mit einer Funktion verbunden sind.