Qual è la differenza tra i tipi di sé e l'eredità dei tratti in Scala?


9

Quando si utilizza Google, vengono visualizzate molte risposte per questo argomento. Tuttavia, non credo che nessuno di loro faccia un buon lavoro nell'illustrare la differenza tra queste due caratteristiche. Quindi mi piacerebbe provare ancora una volta, in particolare ...

Che cosa si può fare con i tipi di sé e non con l'eredità e viceversa?

Per me, ci dovrebbe essere una differenza fisica quantificabile tra i due, altrimenti sono solo nominalmente diversi.

Se il tratto A estende B o i tipi di sé B, non illustrano entrambi che essere un B è un requisito? Dov'è la differenza?


Sono diffidente nei confronti dei termini che hai impostato sulla taglia. Per prima cosa, definire la differenza "fisica", dato che si tratta di tutto il software. Oltre a ciò, per qualsiasi oggetto composito che crei con i mixin, puoi probabilmente creare qualcosa di approssimativo in funzione con l'ereditarietà - se definisci la funzione esclusivamente in termini di metodi visibili. Dove differiranno è in estensibilità, flessibilità e compostabilità.
itsbruce,

Se disponi di un assortimento di piastre in acciaio di dimensioni diverse, potresti avvitarle insieme per formare una scatola o saldarle. Da una prospettiva ristretta, questi sarebbero equivalenti in termini di funzionalità - se si ignora il fatto che uno può essere facilmente riconfigurato o esteso e l'altro no. Ho la sensazione che starai sostenendo che sono equivalenti, anche se sarei felice di essere smentito se avessi detto di più sui tuoi criteri.
itsbruce,

Ho più che familiarità con ciò che stai dicendo in generale, ma non riesco ancora a capire quale sia la differenza in questo caso particolare. Potresti fornire alcuni esempi di codice che dimostrano che un metodo è più estensibile e flessibile dell'altro? * Codice base con estensione * Codice base con tipi di auto * Funzione aggiunta allo stile di estensione * Funzione aggiunta allo stile di tipo auto
Mark Canlas

OK, credo di poter provare che prima della generosità si esaurisce;)
itsbruce

Risposte:


11

Se il tratto A si estende in B, la miscelazione in A ti dà esattamente B più qualunque cosa A aggiunga o si estenda. Al contrario, se la caratteristica A ha un riferimento personale che è esplicitamente tipizzato come B, allora anche la classe genitore finale deve mescolarsi in B o un tipo discendente di B (e mescolarlo per primo , il che è importante).

Questa è la differenza più importante. Nel primo caso, il tipo preciso di B viene cristallizzato nel punto A che lo estende. Nel secondo, il progettista della classe genitore decide quale versione di B viene utilizzata, nel punto in cui è composta la classe genitore.

Un'altra differenza è dove A e B forniscono metodi con lo stesso nome. Dove A estende B, il metodo A ignora B. Dove A viene mischiato dopo B, il metodo A vince semplicemente.

L'auto-riferimento tipizzato ti dà molta più libertà; l'accoppiamento tra A e B è lento.

AGGIORNARE:

Dal momento che non sei chiaro sul vantaggio di queste differenze ...

Se usi l'ereditarietà diretta, crei il tratto A che è B + A. Hai impostato la relazione in pietra.

Se usi un riferimento personale tipizzato, allora chiunque voglia usare il tratto A in classe C potrebbe farlo

  • Mescola B e poi A in C.
  • Mescola un sottotipo di B e poi A in C.
  • Mescola A in C, dove C è una sottoclasse di B.

E questo non è il limite delle loro opzioni, dato il modo in cui Scala ti consente di creare un'istanza di un tratto direttamente con un blocco di codice come suo costruttore.

Per quanto riguarda la differenza tra il metodo di A vincente , poiché A è mischiato per ultimo, rispetto ad A che si estende B, considera questo ...

Quando mescoli in una sequenza di tratti, ogni volta che foo()viene invocato il metodo , il compilatore passa all'ultimo tratto mischiato a cercare foo(), quindi (se non trovato), attraversa la sequenza a sinistra fino a quando non trova un tratto che implementa foo()e usa quello. A ha anche l'opzione di chiamare super.foo(), che attraversa anche la sequenza a sinistra fino a quando non trova un'implementazione, e così via.

Quindi se A ha un auto-riferimento tipizzato a B e lo scrittore di A sa che B implementa foo(), A può chiamare super.foo()sapendo che se non altro fornisce foo(), B lo farà. Tuttavia, il creatore di classe C ha la possibilità di eliminare qualsiasi altro tratto in cui si implementa foo()e A invece lo otterrà.

Ancora una volta, questo è molto più potente e meno limitante di A estendendo B e chiamando direttamente la versione di B di foo().


Qual è la differenza funzionale tra A vincente e A prevalente? Ricevo A in entrambi i casi tramite meccanismi diversi? E nel tuo primo esempio ... Nel tuo primo paragrafo, perché non avere il tratto A estendere SuperOfB? Sembra che potremmo sempre rimodellare il problema usando uno dei due meccanismi. Immagino di non vedere un caso d'uso in cui ciò non è possibile. O sto assumendo troppe cose.
Mark Canlas,

Uhm, perché si vuole avere una estendere una sottoclasse di B, se definisce Bed cosa avete bisogno? L'autoreferenzialità forza la presenza di B (o una sottoclasse), ma offre la scelta dello sviluppatore? Possono mescolarsi in qualcosa che hanno scritto dopo aver scritto il tratto A, purché si estenda B. Perché vincolarli solo a ciò che era disponibile quando hai scritto il tratto A?
itsbruce

Aggiornato per chiarire la differenza.
itsbruce,

@itsbruce c'è qualche differenza concettuale? IS-A contro HAS-A?
Jas,

@Jas Nel contesto della relazione tra i tratti A e B , l'ereditarietà è IS-A mentre l'autoreferenziazione tipizzata dà HAS-A (una relazione compositiva). Per la classe in cui i tratti sono mescolati, il risultato è IS-A , indipendentemente.
itsbruce

0

Ho un po 'di codice che illustra alcune delle differenze rispetto alla visibilità e alle implementazioni "predefinite" quando si estende rispetto all'impostazione di un self-type. Non illustra nessuna delle parti già discusse su come vengono risolte le effettive collisioni di nomi, ma si concentra invece su ciò che è possibile e non è possibile fare.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

Un'importante differenza dell'IMO è che A1non espone che è necessario Ba tutto ciò che lo vede semplicemente A1(come illustrato nelle parti up-cast). L'unico codice che vedrà effettivamente che Bviene utilizzata una particolare specializzazione , è il codice che conosce esplicitamente il tipo composto (come Think*{X,Y}).

Un altro punto è che A2(con estensione) verrà effettivamente utilizzato Bse non viene specificato nient'altro mentre A1(auto-tipo) non dice che utilizzerà a Bmeno che non venga sovrascritto, un B concreto deve essere dato esplicitamente quando gli oggetti vengono istanziati.

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.