Risposte:
Mi vengono in mente due differenze
C'è una sezione di Programmazione in Scala chiamata "Trattare o non trarre?" che affronta questa domanda. Dal momento che il 1 ° ed è disponibile online, spero sia giusto citare il tutto qui. (Qualsiasi serio programmatore di Scala dovrebbe acquistare il libro):
Ogni volta che implementerai una raccolta riutilizzabile di comportamenti, dovrai decidere se vuoi usare un tratto o una classe astratta. Non esiste una regola precisa, ma questa sezione contiene alcune linee guida da considerare.
Se il comportamento non verrà riutilizzato , renderlo una classe concreta. Dopo tutto, non è un comportamento riutilizzabile.
Se può essere riutilizzato in più classi non correlate, rendilo un tratto. Solo i tratti possono essere mescolati in diverse parti della gerarchia di classi.
Se si desidera ereditare da esso nel codice Java , utilizzare una classe astratta. Poiché i tratti con codice non hanno un analogo Java vicino, tende a essere scomodo da ereditare da un tratto in una classe Java. Ereditare da una classe Scala, nel frattempo, è esattamente come ereditare da una classe Java. Come eccezione, un tratto Scala con solo membri astratti si traduce direttamente in un'interfaccia Java, quindi dovresti sentirti libero di definire tali tratti anche se ti aspetti che il codice Java erediti da esso. Vedi il Capitolo 29 per maggiori informazioni su come lavorare insieme a Java e Scala.
Se prevedi di distribuirlo in forma compilata e ti aspetti che i gruppi esterni scrivano classi che ereditano da esso, potresti inclinarti verso l'utilizzo di una classe astratta. Il problema è che quando un tratto guadagna o perde un membro, tutte le classi che ereditano da esso devono essere ricompilate, anche se non sono cambiate. Se i client esterni chiamano solo il comportamento, invece di ereditare da esso, utilizzare un tratto va bene.
Se l'efficienza è molto importante , incline all'utilizzo di una classe. La maggior parte dei runtime Java rende un'invocazione del metodo virtuale di un membro della classe un'operazione più veloce di una invocazione del metodo di interfaccia. I tratti vengono compilati per le interfacce e quindi possono comportare un leggero sovraccarico di prestazioni. Tuttavia, dovresti fare questa scelta solo se sai che il tratto in questione costituisce un collo di bottiglia nelle prestazioni e hai prove che l'utilizzo di una classe invece risolve effettivamente il problema.
Se ancora non lo sai , dopo aver considerato quanto sopra, inizia a renderlo un tratto. Puoi sempre cambiarlo in un secondo momento, e in generale l'utilizzo di un tratto mantiene aperte più opzioni.
Come menzionato @Mushtaq Ahmed, un tratto non può avere alcun parametro passato al costruttore principale di una classe.
Un'altra differenza è il trattamento di super
.
L'altra differenza tra classi e tratti è che mentre nelle classi, le
super
chiamate sono staticamente vincolate, nei tratti sono legate dinamicamente. Se scrivisuper.toString
in una classe, sai esattamente quale implementazione del metodo verrà invocata. Quando si scrive la stessa cosa in un tratto, tuttavia, l'implementazione del metodo da invocare per la super chiamata non è definita quando si definisce il tratto.
Vedi il resto del capitolo 12 per maggiori dettagli.
Modifica 1 (2013):
C'è una sottile differenza nel modo in cui le classi astratte si comportano rispetto ai tratti. Una delle regole di linearizzazione è che preserva la gerarchia ereditaria delle classi, che tende a spingere le classi astratte più avanti nella catena mentre i tratti possono essere felicemente mescolati. In alcune circostanze, è preferibile trovarsi in quest'ultima posizione della linearizzazione delle classi , quindi le classi astratte potrebbero essere utilizzate per questo. Vedi linearizzazione di classe vincolante (ordine di mixin) in Scala .
Modifica 2 (2018):
A partire da Scala 2.12, il comportamento di compatibilità binaria del tratto è cambiato. Prima della 2.12, l'aggiunta o la rimozione di un membro al tratto richiedeva la ricompilazione di tutte le classi che ereditano il tratto, anche se le classi non sono cambiate. Ciò è dovuto al modo in cui i tratti sono stati codificati in JVM.
A partire da Scala 2.12, i tratti vengono compilati per le interfacce Java , quindi il requisito si è leggermente attenuato. Se il tratto esegue una delle seguenti operazioni, le sue sottoclassi richiedono comunque la ricompilazione:
- definizione dei campi (
val
ovar
, ma una costante è ok -final val
senza tipo di risultato)- chiamata
super
- dichiarazioni di inizializzatore nel corpo
- estendere una classe
- basandosi sulla linearizzazione per trovare implementazioni nel supertrait giusto
Ma se il tratto non lo fa, ora puoi aggiornarlo senza interrompere la compatibilità binaria.
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine
- Qualcuno potrebbe spiegare qual è la differenza qui? extends
vs with
?
extends
e with
. È puramente sintattico. Se erediti da più modelli, il primo ottiene extend
, tutti gli altri ottengono with
, tutto qui. Pensate a with
come una virgola: class Foo extends Bar, Baz, Qux
.
Per quanto valga la pena, la Programmazione di Scala di Odersky et al. Raccomanda che, in caso di dubbio, usi i tratti. Puoi sempre cambiarle in classi astratte in seguito, se necessario.
A parte il fatto che non puoi estendere direttamente più classi astratte, ma puoi mescolare più tratti in una classe, vale la pena ricordare che i tratti sono impilabili, poiché le super chiamate in un tratto sono legate dinamicamente (si riferisce a una classe o tratto miscelato prima quello attuale).
Dalla risposta di Thomas nella differenza tra classe astratta e tratto :
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
Nella programmazione di Scala gli autori affermano che le classi astratte creano una relazione "is-a" classica orientata agli oggetti mentre i tratti sono una scala-modo di composizione.
Le classi astratte possono contenere comportamenti: possono essere parametrizzate con args del costruttore (che i tratti non possono) e rappresentano un'entità funzionante. I tratti invece rappresentano solo una singola caratteristica, un'interfaccia di una funzionalità.
trait Enumerable
con molte funzioni di supporto, non li chiamerei comportamento ma solo funzionalità connesse con una caratteristica.