Was ist der Unterschied zwischen Selbsttypen und Vererbung von Merkmalen in Scala?

9

Beim Googeln werden viele Antworten zu diesem Thema angezeigt. Ich habe jedoch nicht das Gefühl, dass einer von ihnen den Unterschied zwischen diesen beiden Merkmalen gut veranschaulicht. Also würde ich es gerne noch einmal versuchen, speziell ...

Was kann mit Selbsttypen und nicht mit Vererbung getan werden und umgekehrt?

Für mich sollte es einen quantifizierbaren physikalischen Unterschied zwischen den beiden geben, sonst sind sie nur nominell unterschiedlich.

Wenn Merkmal A B oder Selbsttyp B erweitert, veranschaulichen beide nicht, dass es eine Voraussetzung ist, von B zu sein? Wo ist der Unterschied?

Mark Canlas
quelle
Ich bin vorsichtig mit den Bedingungen, die Sie für das Kopfgeld festgelegt haben. Definieren Sie zum einen den "physischen" Unterschied, da dies alles Software ist. Darüber hinaus können Sie für jedes zusammengesetzte Objekt, das Sie mit Mixins erstellen, wahrscheinlich eine ungefähre Funktion mit Vererbung erstellen - wenn Sie die Funktion nur anhand der sichtbaren Methoden definieren. Sie unterscheiden sich in Erweiterbarkeit, Flexibilität und Zusammensetzbarkeit.
Itsbruce
Wenn Sie eine Auswahl an Stahlplatten unterschiedlicher Größe haben, können Sie diese zu einer Box zusammenschrauben oder schweißen. Aus einer engen Perspektive wären diese Funktionen gleichwertig - wenn Sie die Tatsache ignorieren, dass eine einfach neu konfiguriert oder erweitert werden kann und die andere nicht. Ich habe das Gefühl, dass Sie argumentieren werden, dass sie gleichwertig sind, obwohl ich mich freuen würde, wenn ich mehr über Ihre Kriterien sagen würde.
Itsbruce
Ich bin mehr als vertraut mit dem, was Sie allgemein sagen, aber ich verstehe immer noch nicht, was der Unterschied in diesem speziellen Fall ist. Können Sie einige Codebeispiele bereitstellen, die zeigen, dass eine Methode erweiterbarer und flexibler ist als die andere? * Basiscode mit Erweiterung *
Basiscode mit Selbsttypen
OK, denke ich kann das versuchen, bevor das Kopfgeld ausgeht;)
itsbruce

Antworten:

11

Wenn Merkmal A B erweitert, erhalten Sie durch Einmischen von A genau B plus alles, was A hinzufügt oder erweitert. Wenn im Gegensatz dazu Merkmal A eine Selbstreferenz hat, die explizit als B eingegeben wird, muss die ultimative übergeordnete Klasse auch B oder einen Nachkommen-Typ von B einmischen (und diese zuerst einmischen , was wichtig ist).

Das ist der wichtigste Unterschied. Im ersten Fall kristallisiert der genaue Typ von B an dem Punkt, an dem A ihn erweitert. Im zweiten Fall kann der Designer der übergeordneten Klasse entscheiden, welche Version von B an dem Punkt verwendet wird, an dem die übergeordnete Klasse zusammengesetzt ist.

Ein weiterer Unterschied besteht darin, dass A und B gleichnamige Methoden bereitstellen. Wenn A B erweitert, überschreibt die Methode von A die von B. Wenn A nach B eingemischt wird, gewinnt einfach die Methode von A.

Die getippte Selbstreferenz gibt Ihnen viel mehr Freiheit; Die Kopplung zwischen A und B ist locker.

AKTUALISIEREN:

Da Sie sich über den Nutzen dieser Unterschiede nicht im Klaren sind ...

Wenn Sie die direkte Vererbung verwenden, erstellen Sie das Merkmal A, das B + A ist. Sie haben die Beziehung in Stein gemeißelt.

Wenn Sie eine typisierte Selbstreferenz verwenden, kann dies jeder, der Ihr Merkmal A in Klasse C verwenden möchte

  • Mischen Sie B und dann A in C.
  • Mischen Sie einen Subtyp von B und dann A in C.
  • Mischen Sie A mit C, wobei C eine Unterklasse von B ist.

Und dies ist nicht die Grenze ihrer Optionen, da Sie mit Scala ein Merkmal direkt mit einem Codeblock als Konstruktor instanziieren können.

Betrachten Sie den Unterschied zwischen dem Gewinn der Methode von A , da A zuletzt gemischt wird, im Vergleich zu A, das B erweitert, Folgendes:

Wenn Sie eine Folge von Merkmalen foo()einmischen, geht der Compiler bei jedem Aufruf der Methode zum zuletzt eingemischten Merkmal, um foo()danach zu suchen , und durchläuft (falls nicht gefunden) die Folge nach links, bis er ein Merkmal findet, das implementiert foo()und verwendet wird Das. A hat auch die Option zum Aufrufen super.foo(), wodurch auch die Sequenz nach links durchlaufen wird, bis eine Implementierung gefunden wird, und so weiter.

Wenn also A eine typisierte Selbstreferenz zu B hat und der Verfasser von A weiß, dass B implementiert foo(), kann A anrufen und super.foo()wissen, dass B es tun wird, wenn nichts anderes vorsieht foo(). Der Ersteller der Klasse C hat jedoch die Möglichkeit, jedes andere Merkmal, in dem implementiert wird foo(), zu löschen , und A erhält dies stattdessen.

Auch dies ist viel leistungsfähiger und weniger einschränkend als A, das B erweitert und direkt die Version von B aufruft foo().

itsbruce
quelle
Was ist der funktionale Unterschied zwischen A-Gewinnen und A-Überschreiben? Ich bekomme A in beiden Fällen über verschiedene Mechanismen? Und in Ihrem ersten Beispiel ... Warum sollte in Ihrem ersten Absatz nicht Merkmal A SuperOfB erweitern? Es fühlt sich einfach so an, als könnten wir das Problem mit beiden Mechanismen jederzeit umgestalten. Ich glaube, ich sehe keinen Anwendungsfall, in dem dies nicht möglich ist. Oder ich gehe von zu vielen Dingen aus.
Mark Canlas
Ähm, warum würden Sie wollen , haben A eine Unterklasse von B erstrecken, wenn B definiert , was Sie brauchen? Die Selbstreferenz zwingt B (oder eine Unterklasse), vorhanden zu sein, gibt aber dem Entwickler die Wahl? Sie können etwas einmischen, das sie geschrieben haben, nachdem Sie Merkmal A geschrieben haben, solange es B erweitert. Warum sollten Sie sie nur auf das beschränken, was verfügbar war, als Sie Merkmal A geschrieben haben?
Itsbruce
Aktualisiert, um den Unterschied deutlich zu machen.
Itsbruce
@itsbruce gibt es einen konzeptionellen Unterschied? IS-A gegen HAS-A?
Jas
@Jas Im Kontext der Beziehung zwischen den Merkmalen A und B ist die Vererbung IS-A, während die typisierte Selbstreferenz HAS-A (eine kompositorische Beziehung) ergibt . Für die Klasse, in die die Merkmale eingemischt sind, ist das Ergebnis unabhängig davon IS-A .
Itsbruce
0

Ich habe einen Code, der einige der Unterschiede zwischen Sichtbarkeit und "Standard" -Implementierungen beim Erweitern und Festlegen eines Selbsttyps veranschaulicht. Es zeigt keinen der bereits besprochenen Teile darüber, wie tatsächliche Namenskollisionen gelöst werden, sondern konzentriert sich auf das, was möglich und was nicht möglich ist.

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
}

Ein wichtiger Unterschied ist, dass IMO A1nicht aussetzt, dass es Bfür irgendetwas benötigt wird , das es lediglich als A1(wie in den hochgearbeiteten Teilen dargestellt) betrachtet. Der einzige Code, der tatsächlich erkennt, dass eine bestimmte Spezialisierung von Bverwendet wird, ist Code, der den zusammengesetzten Typ (wie Think*{X,Y}) explizit kennt .

Ein weiterer Punkt ist, dass A2(mit Erweiterung) tatsächlich verwendet wird, Bwenn nichts anderes angegeben ist, während A1(Selbsttyp) nicht sagt, dass es verwendet wird, Bwenn es nicht überschrieben wird. Ein konkretes B muss explizit angegeben werden, wenn Objekte instanziiert werden.

Rouzbeh Delavari
quelle