Dlaczego mainstreamowe silne statyczne języki OOP zapobiegają dziedziczeniu prymitywów?

53

Dlaczego jest to w porządku i najczęściej oczekiwane:

abstract type Shape
{
   abstract number Area();
}

concrete type Triangle : Shape
{
   concrete number Area()
   {
      //...
   }
}

... podczas gdy to nie jest OK i nikt nie narzeka:

concrete type Name : string
{
}

concrete type Index : int
{
}

concrete type Quantity : int
{
}

Moją motywacją jest maksymalne wykorzystanie systemu typów do weryfikacji poprawności w czasie kompilacji.

PS: tak, przeczytałem to, a owijanie jest hacky obejście.

Legowisko
źródło
1
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
wałek klonowy
Miałem podobną motywację w tym pytaniu , może być dla ciebie interesujące.
default.kramer
Chciałem dodać odpowiedź potwierdzającą ideę „nie chcesz dziedziczenia” i że owijanie jest bardzo potężne, w tym daje ci cokolwiek z jawnych lub jawnych rzutowań (lub awarii), jakie chcesz, szczególnie z optymalizacjami JIT sugerującymi, że będziesz i tak uzyskaj prawie taką samą wydajność, ale powiodłeś się z tą odpowiedzią :-) Chciałbym tylko dodać, byłoby miło, gdyby języki dodały funkcje zmniejszające kod płytki niezbędny do przekazywania właściwości / metod, szczególnie jeśli jest tylko jedna wartość.
Mark Hurd,

Odpowiedzi:

82

Zakładam, że myślisz o językach takich jak Java i C #?

W tych językach prymitywy (jak int) są w zasadzie kompromisem w zakresie wydajności. Nie obsługują wszystkich funkcji obiektów, ale są szybsze i mają mniejszy narzut.

Aby obiekty mogły obsługiwać dziedziczenie, każda instancja musi „wiedzieć” w czasie wykonywania, której klasy jest instancją. W przeciwnym razie zastąpionych metod nie można rozwiązać w czasie wykonywania. W przypadku obiektów oznacza to, że dane instancji są przechowywane w pamięci wraz ze wskaźnikiem do obiektu klasy. Jeśli takie informacje powinny być również przechowywane wraz z prymitywnymi wartościami, wymagania pamięci wzrosną. 16-bitowa wartość całkowita wymagałaby 16 bitów wartości i dodatkowo 32- lub 64-bitowej pamięci dla wskaźnika do swojej klasy.

Oprócz narzutu pamięci można oczekiwać, że można zastąpić typowe operacje na operacjach podstawowych, takich jak operatory arytmetyczne. Bez podtypów operatorzy tacy +mogą zostać skompilowani do prostej instrukcji kodu maszynowego. Jeśli można to zmienić, trzeba będzie rozwiązać metody w czasie wykonywania, co jest znacznie bardziej kosztowną operacją. (Być może wiesz, że C # obsługuje przeciążanie operatora - ale to nie to samo. Przeciążenie operatora jest rozwiązywane w czasie kompilacji, więc nie ma domyślnej kary czasu działania).

Ciągi nie są prymitywne, ale wciąż są „specjalne” pod względem reprezentacji w pamięci. Na przykład są one „internowane”, co oznacza, że ​​dwa literały ciągów, które są równe, można zoptymalizować pod tym samym odniesieniem. Nie byłoby to możliwe (lub co najmniej znacznie mniej skuteczne), gdyby instancje łańcuchowe również śledziły klasę.

To, co opisujesz, byłoby z pewnością przydatne, ale jego obsługa wymagałaby narzutu wydajności dla każdego użycia operacji podstawowych i ciągów, nawet jeśli nie korzystają one z dziedziczenia.

Język Smalltalk pozwala (jak sądzę) na podklasowanie liczb całkowitych. Ale kiedy zaprojektowano Javę, Smalltalk uważano za zbyt wolny, a narzut związany z tym, że wszystko było obiektem, był uważany za jeden z głównych powodów. Java poświęciła trochę elegancji i konceptualnej czystości, aby uzyskać lepszą wydajność.

JacquesB
źródło
12
@Den: stringjest zapieczętowany, ponieważ ma się zachowywać niezmienny. Gdyby można dziedziczyć po łańcuchach, możliwe byłoby tworzenie łańcuchów zmiennych, co spowodowałoby, że naprawdę byłby podatny na błędy. Mnóstwo kodu, w tym samego frameworku .NET, opiera się na ciągach znaków bez efektów ubocznych. Zobacz także tutaj, mówi to samo: quora.com/Dlaczego-String-class-in-C-is-a-sealed-class
Doc Brown
5
@DocBrown Jest to również przyczyna Stringoznaczona również finalw Javie.
Dev.
47
„kiedy zaprojektowano Javę, Smalltalk było uważane za zbyt wolne […]. Java poświęciła trochę elegancji i konceptualnej czystości, aby uzyskać lepszą wydajność”. - Jak na ironię, Java faktycznie nie osiągnęła tej wydajności, dopóki Sun nie kupił firmy Smalltalk, aby uzyskać dostęp do technologii Smalltalk VM, ponieważ jej własna maszyna JVM działała powoli i wydała HotSpot JVM, nieco zmodyfikowaną maszynę Smalltalk VM.
Jörg W Mittag,
3
@underscore_d: Odpowiedź, do której linkujesz, wyraźnie stwierdza, że ​​C♯ nie ma typów pierwotnych. Jasne, niektóre platformy, dla których istnieje implementacja C♯, mogą, ale nie muszą, mieć typy pierwotne, ale to nie znaczy, że C♯ ma typy pierwotne. Na przykład istnieje implementacja Ruby dla CLI, a CLI ma typy pierwotne, ale to nie znaczy, że Ruby ma typy pierwotne. Implementacja może, ale nie musi, zdecydować się na implementację typów wartości poprzez odwzorowanie ich na prymitywne typy platformy, ale jest to prywatny wewnętrzny szczegół implementacji, a nie część specyfikacji.
Jörg W Mittag,
10
Chodzi o abstrakcję. Musimy zachować ostrożność, w przeciwnym razie otrzymamy nonsens. Na przykład: C♯ jest zaimplementowany w .NET. .NET jest zaimplementowany w systemie Windows NT. Windows NT jest zaimplementowany na x86. x86 jest implementowany na dwutlenku krzemu. SiO₂ to po prostu piasek. A więc stringw C♯ jest tylko piaskiem? Nie, oczywiście, że nie, a stringw C♯ jest tym, co mówi specyfikacja C♯. Sposób jego wdrożenia jest nieistotny. Natywna implementacja C♯ implementowałaby ciągi jako tablice bajtów, implementacja ECMAScript zamapowałaby je na ECMAScript Stringitp.
Jörg W Mittag
20

To, co proponuje niektóre języki, nie jest podklasowaniem, ale podtytułem . Na przykład Ada pozwala tworzyć pochodne typy lub podtypy . Ada Programowanie / Typ systemu rozdział warto przeczytać, aby zrozumieć wszystkie szczegóły. Możesz ograniczyć zakres wartości, który jest tym, czego chcesz przez większość czasu:

 type Angle is range -10 .. 10;
 type Hours is range 0 .. 23; 

Możesz użyć obu typów jako liczb całkowitych, jeśli je przekonwertujesz. Zauważ też, że nie możesz użyć jednego zamiast drugiego, nawet jeśli zakresy są strukturalnie równoważne (typy są sprawdzane według nazw).

 type Reference is Integer;
 type Count is Integer;

Powyższe typy są niezgodne, mimo że reprezentują ten sam zakres wartości.

(Ale możesz użyć Unchecked_Conversion; nie mów ludziom, że ci to powiedziałem)

rdzeń rdzeniowy
źródło
2
Właściwie myślę, że bardziej chodzi o semantykę. Korzystanie ilość gdzie jest oczekiwany indeks będzie wówczas nadzieją spowodować błąd kompilacji czas
Marjan Venema
@MarjanVenema Robi to i ma to na celu wyłapanie błędów logicznych.
coredump
Chodzi mi o to, że nie we wszystkich przypadkach, w których potrzebujesz semantyki, potrzebujesz zakresów. Miałbyś wtedy type Index is -MAXINT..MAXINT;coś, co jakoś mi nie pomoże, ponieważ wszystkie liczby całkowite byłyby ważne? Jaki więc błąd popełniłbym przekazując kąt do indeksu, gdyby sprawdzane były tylko zakresy?
Marjan Venema,
1
@MarjanVenema W drugim przykładzie oba typy są podtypami liczb całkowitych. Jeśli jednak zadeklarujesz funkcję, która akceptuje liczbę, nie możesz przekazać referencji, ponieważ sprawdzanie typu opiera się na równoważności nazw , co jest przeciwieństwem „wszystko, co jest sprawdzane, to zakresy”. Nie jest to ograniczone do liczb całkowitych, można użyć wyliczonych typów lub rekordów. ( archive.adaic.com/standards/83rat/html/ratl-04-03.html )
zrzut ekranu
1
@Marjan Jeden miły przykład tego, dlaczego typy tagowania mogą być dość potężne, można znaleźć w serii Erica Lipperta na temat implementacji Zorg w OCaml . W ten sposób kompilator może wykryć wiele błędów - z drugiej strony, jeśli pozwalasz na niejawną konwersję typów, wydaje się, że ta funkcja jest bezużyteczna. Nie ma sensu semantycznego przypisywanie typu PersonAge do typu PersonId. ponieważ oba mają ten sam podstawowy typ.
Voo,
16

Myślę, że to może być pytanie X / Y. Istotne punkty, od pytania ...

Moją motywacją jest maksymalne wykorzystanie systemu typów do weryfikacji poprawności w czasie kompilacji.

... i na podstawie komentarza opracowującego:

Nie chcę być w stanie pośrednio zamieniać się nawzajem.

Przepraszam, jeśli czegoś mi brakuje, ale ... Jeśli to są twoje cele, to dlaczego, u licha, mówisz o dziedzictwie? Domniemana substytucyjność jest ... jak ... cała ta sprawa. Wiesz, zasada podstawienia Liskowa?

W rzeczywistości wydaje się, że w rzeczywistości pragniesz pojęcia „silnego typowania” - w którym coś „jest”, np. Pod intwzględem zasięgu i reprezentacji, ale nie można go zastąpić kontekstami, które oczekują inti odwrotnie. Sugeruję poszukiwanie informacji na temat tego terminu i niezależnie od tego, jak może go nazwać wybrany język. Znów jest to dosłownie przeciwieństwo dziedziczenia.

A dla tych, którzy mogą nie lubić odpowiedzi X / Y, myślę, że tytuł może nadal odpowiadać w odniesieniu do LSP. Typy pierwotne są prymitywne, ponieważ robią coś bardzo prostego i to wszystko, co robią . Pozwolenie im na odziedziczenie, a tym samym uczynienie nieskończonymi ich możliwymi skutkami, doprowadziłoby w najlepszym razie do wielkiej niespodzianki, aw najgorszym przypadku do śmiertelnego naruszenia. Jeśli mogę optymistycznie założyć, że Thales Pereira nie będzie miał nic przeciwko cytowaniu tego fenomenalnego komentarza:

Istnieje dodatkowy problem polegający na tym, że gdyby ktoś mógł odziedziczyć po Int, miałby niewinny kod, taki jak „int x = y + 2” (gdzie Y jest klasą pochodną), który zapisuje dziennik w bazie danych, otwiera adres URL i jakoś wskrzesił Elvisa. Typy pierwotne powinny być bezpieczne i mieć mniej lub bardziej gwarantowane, dobrze określone zachowanie.

Jeśli ktoś zobaczy prymitywny typ w zdrowym języku, słusznie zakłada, że ​​zawsze zrobi to jedno, bardzo dobrze, bez niespodzianek. Typy pierwotne nie mają dostępnych deklaracji klas, które sygnalizują, czy mogą być dziedziczone i czy ich metody są zastępowane. Gdyby tak było, byłoby to bardzo zaskakujące (i całkowicie zerwać z kompatybilnością wsteczną, ale wiem, że to odpowiedź wstecz na „dlaczego X nie został zaprojektowany z Y”).

... chociaż, jak wskazał Mooing Duck w odpowiedzi, języki, które pozwalają na przeciążenie operatora, pozwalają użytkownikowi pomylić się w podobnym lub równym stopniu, jeśli naprawdę tego chcą, więc wątpliwe jest, czy ten ostatni argument się utrzymuje. I przestanę teraz streszczać komentarze innych ludzi, heh.

podkreślenie_d
źródło
4

Aby umożliwić dziedziczenie z wirtualną wysyłką 8, co często jest uważane za bardzo pożądane w projektowaniu aplikacji), potrzebne są informacje o typie środowiska wykonawczego. Dla każdego obiektu muszą być przechowywane niektóre dane dotyczące typu obiektu. Prymityw, z definicji, nie ma tych informacji.

Istnieją dwa (zarządzane, uruchamiane na maszynie wirtualnej) główne języki OOP, które zawierają prymitywy: C # i Java. Wiele innych języków nie ma w ogóle prymitywów lub używa podobnego rozumowania, aby zezwolić na ich użycie.

Prymitywy to kompromis w zakresie wydajności. Dla każdego obiektu potrzebujesz miejsca na jego nagłówek obiektu (w Javie, zwykle 2 * 8 bajtów na 64-bitowych maszynach wirtualnych) oraz jego pola i ewentualne wypełnienie (w Hotspot każdy obiekt zajmuje liczbę bajtów, która jest wielokrotnością 8). Tak więc intobiekt as wymagałby przechowywania co najmniej 24 bajtów pamięci zamiast 4 bajtów (w Javie).

W związku z tym dodano typy pierwotne w celu poprawy wydajności. Ułatwiają wiele rzeczy. Co a + bznaczy, jeśli oba są podtypami int? Aby wybrać poprawne dodanie, należy dodać pewien rodzaj rozwagi. Oznacza to wirtualną wysyłkę. Możliwość użycia bardzo prostego kodu dodawania jest znacznie, dużo szybsza i pozwala na optymalizację czasu kompilacji.

Stringto inna sprawa. Zarówno w Javie, jak i C # Stringjest obiektem. Ale w C # jest zapieczętowany, a w Javie jego ostateczny. To dlatego, że zarówno biblioteki Java, jak i C # wymagają, Stringaby s było niezmienne, a ich podklasowanie złamałoby tę niezmienność.

W przypadku Javy maszyna wirtualna może (i robi) internować ciągi znaków i „pulować” je, co pozwala na lepszą wydajność. Działa to tylko wtedy, gdy ciągi są naprawdę niezmienne.

Ponadto rzadko trzeba podklasować prymitywne typy. Dopóki prymitywów nie da się podklasować, istnieje wiele schludnych rzeczy, które matematyka mówi nam o nich. Na przykład możemy być pewni, że dodawanie jest przemienne i asocjacyjne. To coś, co mówi nam matematyczna definicja liczb całkowitych. Ponadto w wielu przypadkach możemy łatwo profilować niezmienniki za pomocą pętli. Jeśli pozwolimy na podklasę int, tracimy narzędzia, które daje nam matematyka, ponieważ nie możemy już zagwarantować, że pewne właściwości zachowają. W ten sposób, powiedziałbym, zdolność nie móc podklasy prymitywnych typów jest rzeczywiście dobrą rzeczą. Mniej rzeczy, które ktoś może złamać, a kompilator często może udowodnić, że wolno mu dokonywać pewnych optymalizacji.

Polygnome
źródło
1
Ta odpowiedź jest ogromna ... wąska. to allow inheritance, one needs runtime type information.Fałszywe. For every object, some data regarding the type of the object has to be stored.Fałszywe. There are two mainstream OOP languages that feature primitives: C# and Java.Co, czy C ++ nie jest teraz głównym nurtem? Użyję tego jako mojej obalenia, ponieważ informacje o typie środowiska wykonawczego terminem C ++. Nie jest to absolutnie wymagane, chyba że używasz dynamic_castlub typeid. I nawet jeśli RTTI jest włączony, dziedziczenie zużywa miejsce tylko wtedy, gdy klasa ma virtualmetody, na które należy wskazać tabelę metod dla poszczególnych klas na instancję
underscore_d
1
Dziedziczenie w C ++ działa zupełnie inaczej niż w językach uruchamianych na maszynie wirtualnej. wirtualna wysyłka wymaga RTTI, czegoś, co nie było oryginalnie częścią C ++. Dziedziczenie bez wirtualnej wysyłki jest bardzo ograniczone i nie jestem nawet pewien, czy powinieneś porównać go z dziedziczeniem z wirtualną wysyłką. Co więcej, pojęcie „obiektu” jest bardzo różne w C ++ niż w języku C # lub Java. Masz rację, jest kilka rzeczy, które mógłbym lepiej wyrazić, ale szybkie dostanie się do wszystkich dość zaangażowanych kwestii prowadzi do konieczności napisania książki o projektowaniu języka.
Polygnome,
3
Nie jest też tak, że „wirtualna wysyłka wymaga RTTI” w C ++. Znowu tylko dynamic_casti typeinfowymagają tego. Wysyłanie wirtualne jest praktycznie realizowane za pomocą wskaźnika do tabeli vt dla konkretnej klasy obiektu, umożliwiając w ten sposób wywołanie odpowiednich funkcji, ale nie wymaga szczegółowości typu i relacji właściwej dla RTTI. Kompilator musi tylko wiedzieć, czy klasa obiektu jest polimorficzna, a jeśli tak, to jaka jest vptr instancji. Można w prosty sposób kompilować wirtualnie wysyłane klasy -fno-rtti.
underscore_d
2
W rzeczywistości jest odwrotnie, RTTI wymaga wirtualnej wysyłki. Dosłownie -C ++ nie pozwala dynamic_castna klasy bez wirtualnej wysyłki. Powodem implementacji jest to, że RTTI jest generalnie implementowane jako ukryty element vtable.
MSalters
1
@MilesRout C ++ ma wszystko, czego język potrzebuje do OOP, przynajmniej nieco nowsze standardy. Można argumentować, że starszym standardom C ++ brakuje niektórych rzeczy, które są potrzebne do języka OOP, ale nawet to jest odcinek. C ++ nie jest językiem OOP wysokiego poziomu , ponieważ pozwala na bardziej bezpośrednią kontrolę nad niektórymi rzeczami na niskim poziomie, ale mimo to pozwala na OOP. (Wysoki poziom / Niski poziom tutaj pod względem abstrakcji , inne języki, takie jak zarządzane, pobierają więcej systemu poza C ++, stąd ich abstrakcja jest wyższa).
Polygnome,
4

W głównych silnych statycznych językach OOP podtypowanie jest postrzegane przede wszystkim jako sposób na rozszerzenie typu i przesłonięcie obecnych metod tego typu.

Aby to zrobić, „obiekty” zawierają wskaźnik do ich typu. Jest to narzut: kod w metodzie, która korzysta z Shapeinstancji, musi najpierw uzyskać dostęp do informacji o typie tej instancji, zanim zna prawidłową Area()metodę wywołania.

Operacja prymitywna zwykle dopuszcza tylko operacje na niej, które mogą tłumaczyć instrukcje w jednym języku maszynowym i nie niosą ze sobą żadnych informacji o typie. Spowolnienie liczby całkowitej, aby ktoś mógł ją podklasować, było wystarczająco nieprzyjemne, aby powstrzymać wszelkie języki, które w ten sposób stały się głównym nurtem.

Więc odpowiedź na:

Dlaczego mainstreamowe silne statyczne języki OOP zapobiegają dziedziczeniu prymitywów?

Jest:

  • Popyt był niewielki
  • I spowodowałoby to, że język byłby zbyt wolny
  • Subtyping był przede wszystkim postrzegany jako sposób na rozszerzenie typu, a nie jako sposób na lepsze (zdefiniowane przez użytkownika) sprawdzanie typu statycznego.

Zaczynamy jednak pojawiać się języki, które pozwalają na sprawdzanie statyczne na podstawie właściwości zmiennych innych niż „type”, na przykład F # ma „wymiar” i „jednostkę”, więc nie można na przykład dodać długości do obszaru .

Istnieją również języki, które pozwalają na „typy zdefiniowane przez użytkownika”, które nie zmieniają (ani nie wymieniają) tego, co robi dany typ, ale jedynie pomagają w statycznym sprawdzaniu typów; patrz odpowiedź Coredumpa.

Ian
źródło
Jednostki miary F # to miła funkcja, choć niestety źle nazwana. Jest to także czas kompilacji, więc nie jest zbyt przydatny, np. Podczas korzystania ze skompilowanego pakietu NuGet. Właściwy kierunek.
Den
Być może warto zauważyć, że „wymiar” nie jest „właściwością inną niż„ typ ””, jest po prostu bogatszym rodzajem tekstu, niż można się przyzwyczaić.
porglezomp,
3

Nie jestem pewien, czy coś przeoczyłem, ale odpowiedź jest raczej prosta:

  1. Definicja prymitywów jest następująca: prymitywne wartości nie są obiektami, typy pierwotne nie są typami obiektów, prymitywy nie są częścią systemu obiektowego.
  2. Dziedziczenie jest cechą systemu obiektowego.
  3. Ergo, prymitywy nie mogą brać udziału w dziedziczeniu.

Zauważ, że tak naprawdę są tylko dwa silne statyczne języki OOP, które mają nawet prymitywy, AFAIK: Java i C ++. (Właściwie nie jestem nawet pewien co do tego ostatniego, nie wiem dużo o C ++, a to, co znalazłem podczas wyszukiwania, było mylące).

W C ++ prymitywy są w zasadzie dziedzictwem odziedziczonym (zamierzona gra słów) z C. Zatem nie biorą one udziału w systemie obiektowym (a więc i dziedziczeniu), ponieważ C nie ma ani systemu obiektowego, ani dziedziczenia.

W Javie prymitywy są wynikiem błędnej próby poprawy wydajności. Prymitywy są również jedynymi typami wartości w systemie, w rzeczywistości nie można pisać typów wartości w Javie i obiekty nie mogą być typami wartości. Pomijając fakt, że prymitywy nie biorą udziału w systemie obiektowym, a zatem idea „dziedziczenia” nie ma nawet sensu, nawet gdybyś mógł je odziedziczyć, nie byłbyś w stanie utrzymać „ wartość ”. Różni się to od np C♯ który robi mieć wartość typów ( structs), które jednak są obiektami.

Inną rzeczą jest to, że niemożność dziedziczenia nie jest tak naprawdę unikalna dla prymitywów. W C♯ structs domyślnie dziedziczą System.Objecti mogą implementować interfaces, ale nie mogą ani dziedziczyć, ani dziedziczyć po classes lub structs. Ponadto sealed classnie można odziedziczyć es. W Javie final classnie można dziedziczyć es.

tl; dr :

Dlaczego mainstreamowe silne statyczne języki OOP zapobiegają dziedziczeniu prymitywów?

  1. prymitywy nie są częścią systemu obiektowego (z definicji, gdyby tak było, nie byłyby prymitywne), idea dziedziczenia jest powiązana z systemem obiektowym, ergo pierwotne dziedziczenie jest sprzecznością pod względem
  2. prymitywy nie są unikalne, wiele innych typów również nie może być dziedziczonych ( finallub sealedw Javie lub C♯, structs w C♯, case classes w Scali)
Jörg W Mittag
źródło
3
Ehm ... Wiem, że to wymawia się jako "C Sharp", ale, ehm
Pan Lister
Myślę, że jesteś w błędzie po stronie C ++. To wcale nie jest czysty język OO. Metody klasowe domyślnie nie są virtual, co oznacza, że ​​nie są zgodne z LSP. Np. std::stringNie jest prymitywne, ale bardzo zachowuje się jak kolejna wartość. Taka semantyka wartości jest dość powszechna, zakłada ją cała część STL C ++.
MSalters
2
„W Javie prymitywy są wynikiem błędnej próby poprawy wydajności”. Myślę, że nie masz pojęcia o wielkości wydajności związanej z implementacją prymitywów jako typów obiektów rozwijanych przez użytkownika. Ta decyzja w Javie jest zarówno celowa, jak i uzasadniona. Wyobraź sobie, że intmusisz przydzielić pamięć dla każdego, którego używasz. Każda alokacja przyjmuje wartość 100ns plus narzut związany z odśmiecaniem. Porównaj to z zużytym cyklem jednego procesora, dodając dwa prymitywne int. Twoje kody Java będą się czołgać, jeśli projektanci języka zdecydują inaczej.
cmaster,
1
@cmaster: Scala nie ma prymitywów, a jego wydajność numeryczna jest dokładnie taka sama jak w Javie. Ponieważ, cóż, kompiluje liczby całkowite w prymitywach JVM int, więc działają dokładnie tak samo. (Scala-native kompiluje je do prymitywnych rejestrów maszynowych, Scala.js kompiluje je do prymitywnych ECMAScript Numbers.) Ruby nie ma prymitywów, ale YARV i Rubinius kompilują liczby całkowite do prymitywnych liczb całkowitych maszyn, JRuby kompiluje je do prymitywów JVM long. Prawie każda implementacja Lisp, Smalltalk lub Ruby używa prymitywów na maszynie wirtualnej . Właśnie tam optymalizacje wydajności…
Jörg W Mittag,
1
… Należą: w kompilatorze, a nie w języku.
Jörg W Mittag,
2

Joshua Bloch w „Skutecznej Javie” zaleca jawne projektowanie w celu dziedziczenia lub zabranianie tego. Klasy prymitywne nie są zaprojektowane do dziedziczenia, ponieważ zostały zaprojektowane jako niezmienne, a umożliwienie dziedziczenia może to zmienić w podklasach, a tym samym złamać zasadę Liskowa i byłoby źródłem wielu błędów.

W każdym razie, dlaczego jest to hacking obejście? Naprawdę powinieneś preferować kompozycję niż dziedziczenie. Jeśli powodem jest wydajność, niż masz rację, a odpowiedź na twoje pytanie jest taka, że ​​nie jest możliwe umieszczenie wszystkich funkcji w Javie, ponieważ analiza wszystkich różnych aspektów dodawania funkcji wymaga czasu. Na przykład Java nie miała Generics przed 1.5.

Jeśli masz dużo cierpliwości, masz szczęście, ponieważ planujesz dodanie klas wartości do Javy, co pozwoli ci stworzyć swoje klasy wartości, które pomogą ci zwiększyć wydajność, a jednocześnie da ci większą elastyczność.

CodesInTheDark
źródło
2

Na poziomie abstrakcyjnym możesz uwzględnić dowolne elementy w projektowanym języku.

Na poziomie implementacji nieuniknione jest, że niektóre z tych rzeczy będą łatwiejsze do wdrożenia, niektóre będą skomplikowane, niektóre mogą być wykonane szybko, inne będą działać wolniej i tak dalej. Aby to wyjaśnić, projektanci często muszą podejmować trudne decyzje i kompromisy.

Na poziomie implementacji jednym z najszybszych sposobów uzyskania dostępu do zmiennej jest znalezienie jej adresu i załadowanie zawartości tego adresu. W większości procesorów znajdują się szczegółowe instrukcje dotyczące ładowania danych z adresów i instrukcje te zwykle muszą wiedzieć, ile bajtów muszą załadować (jeden, dwa, cztery, osiem itd.) I gdzie umieścić dane, które ładują (pojedynczy rejestr, rejestr para, rejestr rozszerzony, inna pamięć itp.). Znając rozmiar zmiennej, kompilator może dokładnie wiedzieć, które instrukcje należy wydać dla zastosowań tej zmiennej. Nie znając wielkości zmiennej, kompilator musiałby zastosować coś bardziej skomplikowanego i prawdopodobnie wolniejszego.

Na poziomie abstrakcyjnym punktem podtypu jest możliwość użycia instancji jednego typu, w których oczekuje się równego lub bardziej ogólnego typu. Innymi słowy, można napisać kod, który oczekuje obiektu określonego typu lub czegokolwiek bardziej pochodnego, nie wiedząc z wyprzedzeniem, co to dokładnie będzie. I oczywiście, ponieważ więcej typów pochodnych może dodawać więcej elementów danych, typ pochodny niekoniecznie musi mieć takie same wymagania dotyczące pamięci jak typy podstawowe.

Na poziomie implementacji nie ma prostego sposobu, aby zmienna o z góry określonym rozmiarze zawierała instancję o nieznanym rozmiarze i była dostępna w sposób, który normalnie nazwiemy wydajnym. Istnieje jednak sposób, aby trochę przesunąć rzeczy i użyć zmiennej nie do przechowywania obiektu, ale do zidentyfikowania obiektu i umożliwienia przechowywania go w innym miejscu. W ten sposób jest odniesienie (np. Adres pamięci) - dodatkowy poziom pośredni, który zapewnia, że ​​zmienna musi przechowywać tylko pewien rodzaj informacji o stałym rozmiarze, o ile możemy znaleźć obiekt na podstawie tych informacji. Aby to osiągnąć, wystarczy załadować adres (stały rozmiar), a następnie możemy pracować jak zwykle, używając przesunięć obiektu, o których wiemy, że są poprawne, nawet jeśli obiekt ten zawiera więcej danych w przesunięciach, których nie znamy. Możemy to zrobić, ponieważ nie

Na poziomie abstrakcyjnym ta metoda pozwala przechowywać (odwołanie do a) stringw objectzmiennej bez utraty informacji, które ją tworzą string. Wszystkie typy mogą dobrze działać w ten sposób i można powiedzieć, że pod wieloma względami jest elegancki.

Jednak na poziomie implementacji dodatkowy poziom pośredni wymaga większej liczby instrukcji i na większości architektur powoduje, że każdy dostęp do obiektu jest nieco wolniejszy. Możesz zezwolić kompilatorowi na wyciśnięcie większej wydajności z programu, jeśli dołączysz w swoim języku niektóre często używane typy, które nie mają takiego dodatkowego poziomu pośredniego (odniesienie). Jednak po usunięciu tego poziomu pośrednictwa kompilator nie może już pozwolić na podtyp w bezpieczny sposób dla pamięci. Dzieje się tak, ponieważ jeśli dodasz więcej elementów danych do swojego typu i przypiszesz do bardziej ogólnego typu, wszelkie dodatkowe elementy danych, które nie mieszczą się w przestrzeni przydzielonej dla zmiennej docelowej, zostaną usunięte.

Theodoros Chatzigiannakis
źródło
1

Ogólnie

Jeśli klasa jest abstrakcyjna (metafora: pudełko z dziurami), OK (nawet wymagane jest coś użytecznego!) Do „wypełnienia dziur”, dlatego podklasujemy klasy abstrakcyjne.

Jeśli klasa jest konkretna (metafora: pudełko pełne), nie można zmieniać istniejącej, ponieważ jeśli jest pełna, jest pełna. Nie mamy miejsca, aby dodać coś więcej w pudełku, dlatego nie powinniśmy podklasować konkretnych klas.

Z prymitywami

Prymitywy są konkretnymi klasami według projektu. Reprezentują coś, co jest dobrze znane, w pełni określone (nigdy nie widziałem prymitywnego typu z czymś abstrakcyjnym, inaczej nie jest już prymitywne) i szeroko stosowane w systemie. Pozwalając na podklasę prymitywnego typu i zapewnić własną implementację innym, którzy polegają na zaprojektowanym zachowaniu prymitywów, może powodować wiele skutków ubocznych i ogromne szkody!

Cętkowany
źródło
Link jest interesującą opinią projektową. Potrzebuje więcej myślenia.
Den
1

Zwykle dziedziczenie nie jest semantyką, której oczekujesz, ponieważ nie możesz zastąpić swojego specjalnego typu tam, gdzie oczekuje się prymitywów. Pożyczanie z twojego przykładu Quantity + Indexnie ma sensu semantycznego, więc relacja dziedziczenia jest relacją niewłaściwą.

Jednak kilka języków ma pojęcie typu wartości , który wyraża rodzaj relacji, którą opisujesz. Scala jest jednym z przykładów. Typ wartości używa operacji podstawowej jako podstawowej reprezentacji, ale ma inną tożsamość klasy i operacje na zewnątrz. To powoduje wydłużenie typu pierwotnego, ale jest bardziej kompozycją niż relacją dziedziczenia.

Karl Bielefeldt
źródło