Jaka jest zaleta korzystania z klas abstrakcyjnych zamiast cech?

371

Jaka jest zaleta korzystania z klasy abstrakcyjnej zamiast cechy (oprócz wydajności)? Wygląda na to, że klasy abstrakcyjne można w większości przypadków zastąpić cechami.

Ralf
źródło

Odpowiedzi:

371

Mogę wymyślić dwie różnice

  1. Klasy abstrakcyjne mogą mieć parametry konstruktora, a także parametry typu. Cechy mogą mieć tylko parametry typu. Dyskutowano, że w przyszłości nawet cechy mogą mieć parametry konstruktora
  2. Klasy abstrakcyjne są w pełni interoperacyjne z Javą. Możesz wywoływać je z kodu Java bez żadnych opakowań. Cechy są w pełni interoperacyjne tylko wtedy, gdy nie zawierają żadnego kodu implementacyjnego
Mushtaq Ahmed
źródło
172
Bardzo ważny dodatek: klasa może odziedziczyć wiele cech, ale tylko jedną klasę abstrakcyjną. Myślę, że powinno to być pierwsze pytanie zadawane przez programistę przy rozważaniu, którego użyć w prawie wszystkich przypadkach.
BAR
15
ratownik: „Cechy są w pełni interoperacyjne tylko wtedy, gdy nie zawierają żadnego kodu implementacyjnego”
Walrus the Cat
2
streszczenie - gdy zbiorowe zachowania definiujące obiekt lub prowadzące do niego (gałąź obiektu), ale jeszcze nie skomponowane jako obiekt (gotowy). Cechy, kiedy trzeba wprowadzić możliwości, tj. Zdolności nigdy nie powstają wraz z tworzeniem obiektu, ewoluują lub są wymagane, gdy obiekt wychodzi z izolacji i musi się komunikować.
Ramiz Uddin,
5
Pomyśl, druga różnica nie istnieje w Javie8.
Duong Nguyen,
14
Zgodnie ze Scalą 2.12 cecha kompiluje się do interfejsu Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith,
209

Istnieje część w Programowaniu w Scali o nazwie „Aby cechować, czy nie cechować?” który odpowiada na to pytanie. Ponieważ pierwsza edycja jest dostępna online, mam nadzieję, że przytoczę tutaj wszystko. (Każdy poważny programista Scala powinien kupić książkę):

Za każdym razem, gdy wdrażasz zbiór zachowań wielokrotnego użytku, będziesz musiał zdecydować, czy chcesz użyć cechy, czy klasy abstrakcyjnej. Nie ma sztywnej zasady, ale w tej sekcji znajduje się kilka wskazówek do rozważenia.

Jeśli zachowanie nie zostanie ponownie wykorzystane , uczyń je konkretną klasą. W końcu nie jest to zachowanie wielokrotnego użytku.

Jeśli można go ponownie wykorzystać w wielu niepowiązanych klasach , uczyń go cechą. Tylko cechy mogą być mieszane w różnych częściach hierarchii klas.

Jeśli chcesz dziedziczyć po nim w kodzie Java , użyj klasy abstrakcyjnej. Ponieważ cechy z kodem nie mają bliskiego analogu Java, odziedziczenie cech po klasie Java jest raczej niewygodne. Tymczasem dziedziczenie z klasy Scala jest dokładnie takie samo jak dziedziczenie z klasy Java. Jako jeden wyjątek, cecha Scala z tylko elementami abstrakcyjnymi tłumaczy bezpośrednio na interfejs Java, więc powinieneś swobodnie definiować takie cechy, nawet jeśli spodziewasz się po nim dziedziczenia kodu Java. Więcej informacji na temat wspólnej pracy z Javą i Scalą znajduje się w rozdziale 29.

Jeśli planujesz dystrybuować go w skompilowanej formie i spodziewasz się, że grupy zewnętrzne będą pisać dziedziczące z niego klasy, możesz skłaniać się ku użyciu klasy abstrakcyjnej. Problem polega na tym, że gdy cecha zyskuje lub traci członka, wszelkie dziedziczące od niej klasy muszą zostać ponownie skompilowane, nawet jeśli nie uległy zmianie. Jeśli klienci zewnętrzni będą wywoływać tylko zachowanie, zamiast dziedziczyć po nim, użycie cechy jest w porządku.

Jeśli wydajność jest bardzo ważna , skłaniaj się ku użyciu klasy. Większość środowisk wykonawczych Java sprawia, że ​​wywołanie metody wirtualnej członka klasy jest szybsze niż wywołanie metody interfejsu. Cechy są kompilowane do interfejsów i dlatego mogą ponieść niewielki koszt wydajności. Jednak powinieneś dokonać tego wyboru tylko wtedy, gdy wiesz, że dana cecha stanowi wąskie gardło wydajności i masz dowody, że użycie klasy faktycznie rozwiązuje problem.

Jeśli nadal nie wiesz , po rozważeniu powyższego, zacznij od uczynienia go cechą. Zawsze możesz go później zmienić i ogólnie rzecz biorąc, użycie cechy pozwala zachować więcej opcji.

Jak wspomniano @Mushtaq Ahmed, cecha nie może mieć żadnych parametrów przekazywanych do głównego konstruktora klasy.

Kolejną różnicą jest leczenie super.

Inna różnica między klasami a cechami polega na tym, że podczas gdy w klasach superwywołania są związane statycznie, w przypadku cech są one dynamicznie powiązane. Jeśli piszesz super.toStringw klasie, wiesz dokładnie, która metoda zostanie wywołana. Kiedy piszesz to samo w cechy, jednak implementacja metody, która ma zostać wywołana dla super wywołania, jest niezdefiniowana podczas definiowania cechy.

Więcej informacji znajduje się w dalszej części rozdziału 12 .

Edycja 1 (2013):

Istnieje niewielka różnica w zachowaniu klas abstrakcyjnych w porównaniu do cech. Jedną z reguł linearyzacji jest to, że zachowuje hierarchię dziedziczenia klas, która ma tendencję do popychania klas abstrakcyjnych na później w łańcuchu, podczas gdy cechy mogą być z powodzeniem mieszane. W pewnych okolicznościach faktycznie lepiej jest znajdować się na drugiej pozycji linearyzacji klas , więc można do tego użyć klas abstrakcyjnych. Zobacz ograniczanie linearyzacji klas (kolejność mieszania) w Scali .

Edycja 2 (2018):

Począwszy od Scali 2.12, zachowanie zgodności binarnej cechy uległo zmianie. Przed 2.12 dodanie lub usunięcie elementu do cechy wymagało ponownej kompilacji wszystkich klas, które dziedziczą tę cechę, nawet jeśli klasy się nie zmieniły. Wynika to ze sposobu, w jaki cechy zostały zakodowane w JVM.

Począwszy od Scali 2.12, cechy kompilują się do interfejsów Java , więc wymagania nieco się zmniejszyły. Jeśli cecha spełnia którekolwiek z poniższych kryteriów, jej podklasy nadal wymagają ponownej kompilacji:

  • definiowanie pól ( vallub var, ale stała jest w porządku - final valbez typu wyniku)
  • powołanie super
  • instrukcje inicjalizujące w treści
  • rozszerzenie klasy
  • opierając się na linearyzacji w celu znalezienia implementacji w prawej nadbudowie

Ale jeśli ta cecha nie, możesz ją teraz zaktualizować bez naruszenia zgodności binarnej.

Eugene Yokota
źródło
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Czy ktoś mógłby wyjaśnić, jaka jest tutaj różnica? extendskontra with?
0
2
@ 0fnt Jego rozróżnienie nie dotyczy przedłużania w porównaniu z. Mówi on, że jeśli wmieszasz tę cechę tylko w tej samej kompilacji, problemy ze zgodnością binarną nie mają zastosowania. Jeśli jednak interfejs API został zaprojektowany tak, aby umożliwić użytkownikom samodzielne mieszanie tej cechy, musisz się martwić o zgodność binarną.
John Colanduoni,
2
@ 0fnt: Absolutnie nie ma semantycznej różnicy między extendsi with. Jest to czysto składniowe. Jeśli odziedziczysz po wielu szablonach, pierwszy dostaje extend, wszystkie inne dostają with, to wszystko. Pomyśl withjak przecinkami: class Foo extends Bar, Baz, Qux.
Jörg W Mittag
77

Niezależnie od tego, co jest warte, Odersky i wsp. Programowanie w Scali zaleca, aby w razie wątpliwości używać cech. W razie potrzeby zawsze możesz zmienić je na klasy abstrakcyjne.

Daniel C. Sobral
źródło
20

Poza tym, że nie można bezpośrednio rozszerzyć wielu klas abstrakcyjnych, ale można łączyć wiele cech w klasę, warto wspomnieć, że cechy można ustawiać jeden na drugim, ponieważ super wywołania cechy są dynamicznie powiązane (odnosi się to do klasy lub cechy mieszanej wcześniej obecne).

Z odpowiedzi Thomasa na Różnice między klasą abstrakcyjną a cechą :

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
Nemanja Boric
źródło
9

Podczas rozszerzania klasy abstrakcyjnej pokazuje to, że podklasa jest podobnego rodzaju. Wydaje mi się, że nie jest tak w przypadku używania cech.

Peter p
źródło
Czy ma to jakieś praktyczne implikacje, czy tylko ułatwia zrozumienie kodu?
Ralf
8

W Programowaniu Scali autorzy twierdzą, że klasy abstrakcyjne tworzą klasyczną relację obiektową typu „jest-a”, podczas gdy cechy są skalowaną kompozycją.

Kuna
źródło
5

Klasy abstrakcyjne mogą zawierać zachowanie - można sparametryzować je za pomocą argumentów konstruktora (których cechy nie mogą) i reprezentować działającą jednostkę. Zamiast tego cechy reprezentują tylko jedną funkcję, interfejs jednej funkcjonalności.

Dario
źródło
8
Mam nadzieję, że nie sugerujesz, że cechy nie mogą zawierać zachowania. Oba mogą zawierać kod implementacyjny.
Mitch Blevins,
1
@Mitch Blevins: Oczywiście, że nie. Mogą zawierać kod, ale kiedy definiujesz za trait Enumerablepomocą wielu funkcji pomocniczych, nie nazwałbym ich zachowaniem, a jedynie funkcjonalnością związaną z jedną funkcją.
Dario
4
@Dario Uważam, że „zachowanie” i „funkcjonalność” są synonimami, więc uważam, że twoja odpowiedź jest bardzo myląca.
David J.
3
  1. Klasa może dziedziczyć po wielu cechach, ale tylko jednej klasie abstrakcyjnej.
  2. Klasy abstrakcyjne mogą mieć parametry konstruktora, a także parametry typu. Cechy mogą mieć tylko parametry typu. Na przykład nie można powiedzieć cecha t (i: Int) {}; parametr i jest nielegalny.
  3. Klasy abstrakcyjne są w pełni interoperacyjne z Javą. Możesz wywoływać je z kodu Java bez żadnych opakowań. Cechy są w pełni interoperacyjne tylko wtedy, gdy nie zawierają żadnego kodu implementacyjnego.
pavan.vn101
źródło