Qual è il vantaggio di usare classi astratte anziché tratti?


371

Qual è il vantaggio di usare una classe astratta invece di una caratteristica (a parte le prestazioni)? Sembra che le classi astratte possano essere sostituite da tratti nella maggior parte dei casi.

Risposte:


372

Mi vengono in mente due differenze

  1. Le classi astratte possono avere parametri di costruzione e parametri di tipo. I tratti possono avere solo parametri di tipo. Si è discusso che in futuro anche i tratti possono avere parametri di costruzione
  2. Le classi astratte sono completamente interoperabili con Java. Puoi chiamarli dal codice Java senza alcun wrapper. I tratti sono completamente interoperabili solo se non contengono alcun codice di implementazione

173
Addendum molto importante: una classe può ereditare da più tratti ma solo una classe astratta. Penso che questa dovrebbe essere la prima domanda che uno sviluppatore pone quando considera quale usare in quasi tutti i casi.
BAR

16
salvagente: "I tratti sono completamente interoperabili solo se non contengono alcun codice di implementazione"
Walrus the Cat

2
abstract - quando i comportamenti collettivi definiscono o conducono a un oggetto (ramo di oggetto) ma non sono ancora composti come oggetti (pronti). Tratti, quando è necessario indurre capacità, cioè le capacità non hanno mai origine con la creazione di oggetti, si evolve o è necessario quando un oggetto esce dall'isolamento e deve comunicare.
Ramiz Uddin,

5
La seconda differenza non esiste in Java8, pensa.
Duong Nguyen,

14
Per Scala 2.12, un tratto si compila in un'interfaccia Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith,

209

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 superchiamate sono staticamente vincolate, nei tratti sono legate dinamicamente. Se scrivi super.toStringin 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 ( valo var, ma una costante è ok - final valsenza 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.


2
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? extendsvs with?
0

2
@ 0fnt La sua distinzione non riguarda extends vs with. Quello che sta dicendo è che se si mescola il tratto all'interno della stessa compilation, i problemi di compatibilità binaria non si applicano. Tuttavia, se la tua API è progettata per consentire agli utenti di mescolare il tratto da soli, dovrai preoccuparti della compatibilità binaria.
John Colanduoni,

2
@ 0fnt: non c'è assolutamente alcuna differenza semantica tra extendse with. È puramente sintattico. Se erediti da più modelli, il primo ottiene extend, tutti gli altri ottengono with, tutto qui. Pensate a withcome una virgola: class Foo extends Bar, Baz, Qux.
Jörg W Mittag,


20

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

9

Quando si estende una classe astratta, questo dimostra che la sottoclasse è di tipo simile. Questo non è necessariamente il caso quando si usano i tratti, penso.


Ciò ha implicazioni pratiche o semplifica la comprensione del codice?
Ralf

8

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.


5

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à.


8
Spero che tu non stia insinuando che i tratti non possono contenere comportamenti. Entrambi possono contenere codice di implementazione.
Mitch Blevins,

1
@Mitch Blevins: Certo che no. Possono contenere codice, ma quando si definiscono trait Enumerablecon molte funzioni di supporto, non li chiamerei comportamento ma solo funzionalità connesse con una caratteristica.
Dario

4
@Dario Vedo "comportamento" e "funzionalità" come sinonimi, quindi trovo la tua risposta molto confusa.
David J.

3
  1. Una classe può ereditare da più tratti ma solo una classe astratta.
  2. Le classi astratte possono avere parametri di costruzione e parametri di tipo. I tratti possono avere solo parametri di tipo. Ad esempio, non puoi dire tratto t (i: Int) {}; il parametro i è illegale.
  3. Le classi astratte sono completamente interoperabili con Java. Puoi chiamarli dal codice Java senza alcun wrapper. I tratti sono completamente interoperabili solo se non contengono alcun codice di implementazione.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.