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.
type-systems
static-typing
strong-typing
Legowisko
źródło
źródło
Odpowiedzi:
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ść.
źródło
string
jest 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-classString
oznaczona równieżfinal
w Javie.string
w C♯ jest tylko piaskiem? Nie, oczywiście, że nie, astring
w 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 ECMAScriptString
itp.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:
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).
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)
źródło
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?Myślę, że to może być pytanie X / Y. Istotne punkty, od pytania ...
... i na podstawie komentarza opracowującego:
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
int
względem zasięgu i reprezentacji, ale nie można go zastąpić kontekstami, które oczekująint
i 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:
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.
źródło
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
int
obiekt 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 + b
znaczy, jeśli oba są podtypamiint
? 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.String
to inna sprawa. Zarówno w Javie, jak i C #String
jest obiektem. Ale w C # jest zapieczętowany, a w Javie jego ostateczny. To dlatego, że zarówno biblioteki Java, jak i C # wymagają,String
aby 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.źródło
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 są terminem C ++. Nie jest to absolutnie wymagane, chyba że używaszdynamic_cast
lubtypeid
. I nawet jeśli RTTI jest włączony, dziedziczenie zużywa miejsce tylko wtedy, gdy klasa mavirtual
metody, na które należy wskazać tabelę metod dla poszczególnych klas na instancjędynamic_cast
itypeinfo
wymagają 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
.dynamic_cast
na klasy bez wirtualnej wysyłki. Powodem implementacji jest to, że RTTI jest generalnie implementowane jako ukryty element vtable.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
Shape
instancji, 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:
Jest:
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.
źródło
Nie jestem pewien, czy coś przeoczyłem, ale odpowiedź jest raczej prosta:
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 (
struct
s), które jednak są obiektami.Inną rzeczą jest to, że niemożność dziedziczenia nie jest tak naprawdę unikalna dla prymitywów. W C♯
struct
s domyślnie dziedzicząSystem.Object
i mogą implementowaćinterface
s, ale nie mogą ani dziedziczyć, ani dziedziczyć poclass
es lubstruct
s. Ponadtosealed
class
nie można odziedziczyć es. W Javiefinal
class
nie można dziedziczyć es.tl; dr :
final
lubsealed
w Javie lub C♯,struct
s w C♯,case class
es w Scali)źródło
virtual
, co oznacza, że nie są zgodne z LSP. Np.std::string
Nie jest prymitywne, ale bardzo zachowuje się jak kolejna wartość. Taka semantyka wartości jest dość powszechna, zakłada ją cała część STL C ++.int
musisz 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 prymitywneint
. Twoje kody Java będą się czołgać, jeśli projektanci języka zdecydują inaczej.int
, więc działają dokładnie tak samo. (Scala-native kompiluje je do prymitywnych rejestrów maszynowych, Scala.js kompiluje je do prymitywnych ECMAScriptNumber
s.) 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 JVMlong
. Prawie każda implementacja Lisp, Smalltalk lub Ruby używa prymitywów na maszynie wirtualnej . Właśnie tam optymalizacje wydajności…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ść.
źródło
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)
string
wobject
zmiennej 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.
źródło
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!
źródło
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 + Index
nie 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.
źródło