Jaka jest różnica między typami własnymi a dziedziczeniem cech w Scali?

9

Gdy Googled, pojawia się wiele odpowiedzi na ten temat. Nie wydaje mi się jednak, aby któryś z nich dobrze ilustrował różnicę między tymi dwiema funkcjami. Chciałbym więc spróbować jeszcze raz, a konkretnie ...

Co można zrobić z typami własnymi, a nie z dziedziczeniem i odwrotnie?

Dla mnie powinna istnieć jakaś wymierna, fizyczna różnica między nimi, w przeciwnym razie są one tylko nominalnie różne.

Jeśli Cecha A rozciąga się na B lub typu B, to czy oba nie pokazują, że bycie B jest wymogiem? Jaka jest różnica?

Mark Canlas
źródło
Uważam na warunki, które wyznaczyłeś na nagrodę. Po pierwsze, zdefiniuj różnicę „fizyczną”, biorąc pod uwagę, że jest to całe oprogramowanie. Poza tym dla każdego obiektu złożonego, który utworzysz za pomocą mixin, możesz prawdopodobnie stworzyć coś w przybliżeniu w funkcji z dziedziczeniem - jeśli zdefiniujesz funkcję wyłącznie w kategoriach widocznych metod. Różnią się między sobą rozszerzalnością, elastycznością i kompozycyjnością.
itsbruce
Jeśli masz asortyment płyt stalowych o różnych rozmiarach, możesz połączyć je ze sobą, tworząc skrzynkę lub spawać. Z jednej wąskiej perspektywy byłyby one równoważne pod względem funkcjonalności - jeśli zignorujesz fakt, że jednego można łatwo zmienić lub rozszerzyć, a drugiego nie. Mam wrażenie, że będziesz argumentować, że są one równoważne, chociaż chętnie popełniłbym błąd, gdybyś powiedział więcej o swoich kryteriach.
itsbruce
Jestem bardziej niż zaznajomiony z tym, co ogólnie mówisz, ale nadal nie rozumiem, jaka jest różnica w tym konkretnym przypadku. Czy możesz podać kilka przykładów kodu, które pokazują, że jedna metoda jest bardziej elastyczna i elastyczna niż druga? * Kod podstawowy z rozszerzeniem * Kod podstawowy z typami własnymi * Funkcja dodana do stylu rozszerzenia * Funkcja dodana do stylu samodzielnego
Mark Canlas 25.11.2013
OK, myślę, że mogę spróbować, zanim skończy się nagroda;)
itsbruce

Odpowiedzi:

11

Jeśli cecha A rozszerza B, wówczas mieszanie w A daje dokładnie B plus to, co A dodaje lub rozszerza. W przeciwieństwie do tego, jeśli cecha A ma własne odwołanie, które jest jawnie wpisane jako B, wówczas najwyższa klasa nadrzędna musi również mieszać B lub potomek B (i mieszać najpierw , co jest ważne).

To najważniejsza różnica. W pierwszym przypadku dokładny typ B krystalizuje się w punkcie A, który go wydłuża. W drugim przypadku projektant klasy nadrzędnej decyduje, która wersja B zostanie użyta w punkcie, w którym klasa macierzysta jest utworzona.

Inna różnica polega na tym, że A i B zapewniają metody o tej samej nazwie. Gdzie A rozszerza B, metoda A zastępuje B. Tam, gdzie A miesza się po B, metoda A po prostu wygrywa.

Wpisane na maszynie odnośniki dają znacznie więcej swobody; połączenie między A i B jest luźne.

AKTUALIZACJA:

Ponieważ nie masz pewności co do korzyści wynikających z tych różnic ...

Jeśli korzystasz z dziedziczenia bezpośredniego, tworzysz cechę A, którą jest B + A. Ułożyłeś związek w kamień.

Jeśli użyjesz samouczenia na maszynie, każdy, kto chce użyć twojej cechy A w klasie C., może

  • Wymieszaj B, a następnie A w C.
  • Wymieszaj podtyp B, a następnie A w C.
  • Wymieszaj A w C, gdzie C jest podklasą B.

I to nie jest granica ich opcji, biorąc pod uwagę sposób, w jaki Scala pozwala na tworzenie instancji cechy bezpośrednio z blokiem kodu jako konstruktorem.

Jeśli chodzi o różnicę między wygraną metody A , ponieważ A jest pomieszane na końcu, w porównaniu do A przedłużającego B, rozważ to ...

Kiedy łączysz sekwencję cech, za każdym razem, gdy foo()wywoływana jest metoda , kompilator przechodzi do ostatniej wymieszanej cechy w celu jej wyszukania foo(), a następnie (jeśli nie została znaleziona), przechodzi przez sekwencję w lewo, aż znajdzie cechę, która implementuje foo()i wykorzystuje że. A ma również opcję wywoływania super.foo(), która również przechodzi przez sekwencję w lewo, aż znajdzie implementację i tak dalej.

Więc jeśli A ma wpisane na maszynie odniesienie do B, a pisarz A wie, że B implementuje foo(), A może zadzwonić super.foo()wiedząc, że jeśli nic innego nie zapewni foo(), B to zrobi. Jednak twórca klasy C ma możliwość upuszczenia dowolnej innej cechy, w której implementuje foo(), a zamiast tego dostanie to.

Ponownie, jest to o wiele bardziej wydajne i mniej ograniczający niż rozciągający B i bezpośrednio dzwoniąc wersja B z użytkownikiem foo().

itsbruce
źródło
Jaka jest funkcjonalna różnica między nadrzędnym wygranym a nadrzędnym? Dostaję A w obu przypadkach za pośrednictwem różnych mechanizmów? I w twoim pierwszym przykładzie ... W pierwszym akapicie, dlaczego nie mieć cechy A przedłużyć SuperOfB? Po prostu wydaje się, że zawsze możemy przerobić problem za pomocą dowolnego z mechanizmów. Chyba nie widzę przypadku użycia, w którym nie jest to możliwe. Albo zakładam zbyt wiele rzeczy.
Mark Canlas,
Dlaczego miałbyś chcieć, aby A rozszerzył podklasę B, jeśli B definiuje to, czego potrzebujesz? Odniesienie własne wymusza obecność B (lub podklasy), ale daje wybór programistom? Mogą mieszać coś, co napisali po tym, jak napisałeś cechę A, o ile rozszerza ona B. Dlaczego ograniczasz je tylko do tego, co było dostępne, kiedy pisałeś cechę A?
itsbruce
Zaktualizowano, aby różnica była bardzo wyraźna.
itsbruce
@itsbruce czy jest jakaś różnica koncepcyjna? IS-A kontra HAS-A?
Jas
@Jas W kontekście relacji między cechami A i B , dziedziczenie to IS-A, natomiast wpisane odwołanie do siebie daje HAS-A (relacja kompozycyjna). Dla klasy, w której cechy są mieszane, wynikiem jest IS-A , niezależnie od tego.
itsbruce
0

Mam kod, który ilustruje niektóre różnice w widoczności wrt i „domyślne” implementacje przy rozszerzaniu vs ustawianie własnego typu. Nie ilustruje żadnej z omawianych już części na temat sposobu rozwiązywania rzeczywistych kolizji nazw, ale skupia się na tym, co jest możliwe i niemożliwe.

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
}

Jedną ważną różnicą jest to, że IMO A1nie ujawnia, że ​​potrzebuje Bwszystkiego, co tylko ją widzi A1(jak pokazano w części up-cast). Jedynym kodem, który faktycznie zobaczy, że Bużywana jest szczególna specjalizacja , jest kod, który wyraźnie wie o typie złożonym (podobnym Think*{X,Y}).

Inną kwestią jest to, że A2(z rozszerzeniem) będzie faktycznie używane, Bjeśli nic innego nie zostanie określone, podczas gdy A1(własny typ) nie powie, że będzie używał, Bchyba że zostanie zastąpione, konkretne B musi zostać wyraźnie podane, gdy obiekty są tworzone.

Rouzbeh Delavari
źródło