Wiem, że struktury w .NET nie obsługują dziedziczenia, ale nie jest do końca jasne, dlaczego są w ten sposób ograniczone.
Jaki powód techniczny uniemożliwia strukturom dziedziczenie po innych strukturach?
Wiem, że struktury w .NET nie obsługują dziedziczenia, ale nie jest do końca jasne, dlaczego są w ten sposób ograniczone.
Jaki powód techniczny uniemożliwia strukturom dziedziczenie po innych strukturach?
Odpowiedzi:
Typy wartości nie mogą obsługiwać dziedziczenia, ponieważ są to tablice.
Problem polega na tym, że ze względów wydajnościowych i GC tablice typów wartości są przechowywane „w linii”. Na przykład, new FooType[10] {...}
jeśli FooType
jest typem referencyjnym, na stercie zarządzanym zostanie utworzonych 11 obiektów (jeden dla tablicy i 10 dla każdego wystąpienia typu). Jeśli FooType
jest zamiast tego typem wartości, tylko jedna instancja zostanie utworzona na zarządzanym stercie - dla samej tablicy (ponieważ każda wartość tablicy będzie przechowywana „w linii” z tablicą).
Załóżmy teraz, że mamy dziedziczenie z typami wartości. W połączeniu z powyższym zachowaniem tablic w „pamięci wbudowanej”, zdarzają się złe rzeczy, co można zobaczyć w C ++ .
Rozważmy ten pseudo-C # kod:
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
Zgodnie z normalnymi regułami konwersji a Derived[]
można zamienić na a Base[]
(na lepsze lub gorsze), więc jeśli użyjesz s / struct / class / g dla powyższego przykładu, skompiluje się i uruchomi zgodnie z oczekiwaniami, bez żadnych problemów. Ale jeśli Base
i Derived
są typami wartości, a tablice przechowują wartości w linii, to mamy problem.
Mamy problem, ponieważ Square()
nic o Derived
tym nie wiemy , użyje tylko arytmetyki wskaźników, aby uzyskać dostęp do każdego elementu tablicy, zwiększając o stałą wartość (sizeof(A)
). Zgromadzenie wyglądałoby mniej więcej tak:
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(Tak, to ohydny zestaw, ale chodzi o to, że będziemy zwiększać poprzez tablicę według znanych stałych czasu kompilacji, bez żadnej wiedzy, że używany jest typ pochodny).
Tak więc, gdyby tak się stało, mielibyśmy problemy z uszkodzeniem pamięci. W szczególności, w ramach Square()
, values[1].A*=2
by rzeczywiście być modyfikujący values[0].B
!
Spróbuj debugowania TEGO !
Wyobraź sobie struktury obsługujące dziedziczenie. Następnie oświadczam:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
oznaczałoby to, że zmienne strukturalne nie mają stałego rozmiaru i dlatego mamy typy referencyjne.
Jeszcze lepiej, rozważ to:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
dziedziczenia struktury Bar
nie powinno pozwalać Foo
na przypisanie a do a Bar
, ale zadeklarowanie struktury w ten sposób może pozwolić na kilka użytecznych efektów: (1) Utwórz specjalnie nazwany element członkowski typu Bar
jako pierwszy element Foo
i Foo
uwzględnij nazwy członków, że alias do tych członków Bar
, pozwalając kodu, które wykorzystywane Bar
być dostosowana do użyć Foo
zamiast tego, bez konieczności wymiany wszystkich odniesień do thing.BarMember
z thing.theBar.BarMember
, i zachowując możliwość odczytu i zapisu wszystkich Bar
„s dziedzinach jak grupy; ...
Struktury nie używają referencji (chyba że są w ramkach, ale powinieneś starać się tego unikać), dlatego polimorfizm nie ma znaczenia, ponieważ nie ma pośrednictwa przez wskaźnik odniesienia. Obiekty normalnie żyją na stercie i odwołują się do nich poprzez wskaźniki referencyjne, ale struktury są alokowane na stosie (chyba że są opakowane) lub są alokowane „wewnątrz” pamięci zajmowanej przez typ referencyjny na stercie.
Foo
który ma pole typu struktury, Bar
który mógłby traktować Bar
elementy członkowskie jako własne, tak aby Point3d
klasa mogła np hermetyzacji Point2d xy
ale odnoszą się do X
tej dziedzinie, jak też xy.X
czy X
.
Oto, co mówią dokumenty :
Struktury są szczególnie przydatne w przypadku małych struktur danych, które mają semantykę wartości. Liczby zespolone, punkty w układzie współrzędnych lub pary klucz-wartość w słowniku to dobre przykłady struktur. Kluczem do tych struktur danych jest to, że mają one niewielu członków danych, nie wymagają dziedziczenia ani tożsamości referencyjnej oraz że można je wygodnie zaimplementować przy użyciu semantyki wartości, w której przypisanie kopiuje wartość zamiast odwołania.
Zasadniczo mają one przechowywać proste dane i dlatego nie mają „dodatkowych funkcji”, takich jak dziedziczenie. Prawdopodobnie byłoby dla nich technicznie możliwe do obsługi jakiegoś ograniczonego rodzaju dziedziczenia (nie polimorfizmu, ponieważ są na stosie), ale uważam, że jest to również wybór projektu, aby nie obsługiwać dziedziczenia (jak wiele innych rzeczy w .NET języki są.)
Z drugiej strony zgadzam się z korzyściami płynącymi z dziedziczenia i myślę, że wszyscy osiągnęliśmy punkt, w którym chcemy, abyśmy struct
odziedziczyli po kimś innym i zdaliśmy sobie sprawę, że to niemożliwe. Ale w tym momencie struktura danych jest prawdopodobnie tak zaawansowana, że i tak powinna być klasą.
Point3D
z a Point2D
; nie byłbyś w stanie aby użyć a Point3D
zamiast a Point2D
, ale nie musiałbyś ponownie implementować Point3D
całkowicie od zera.) Tak i tak to zinterpretowałem ...
class
się struct
w razie potrzeby.
Dziedziczenie takie jak klasa nie jest możliwe, ponieważ struktura jest układana bezpośrednio na stosie. Dziedziczona struktura byłaby większa niż jej rodzic, ale JIT o tym nie wie i stara się umieścić zbyt dużo na zbyt małej przestrzeni. Brzmi trochę niejasno, napiszmy przykład:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
Gdyby to było możliwe, zawiesiłoby się na następującym fragmencie:
void DoSomething(A arg){};
...
B b;
DoSomething(b);
Przestrzeń jest przeznaczona dla rozmiaru A, a nie dla rozmiaru B.
Jest jeden punkt, który chciałbym poprawić. Nawet jeśli powodem, dla którego struktur nie można dziedziczyć, jest to, że żyją na stosie, jest to właściwe, jest to jednocześnie w połowie poprawne wyjaśnienie. Struktury, jak każdy inny typ wartości, mogą znajdować się na stosie. Ponieważ będzie to zależeć od tego, gdzie zadeklarowano zmienną, będą one znajdować się na stosie lub w stercie . Dzieje się tak, gdy są to odpowiednio zmienne lokalne lub pola instancji.
Mówiąc to, Cecil Has a Name ujął to poprawnie.
Chciałbym to podkreślić, typy wartości mogą żyć na stosie. Nie oznacza to, że zawsze to robią. Zmienne lokalne, w tym parametry metody, will. Wszyscy inni nie. Niemniej jednak nadal pozostaje powodem, dla którego nie można ich odziedziczyć. :-)
Struktury są przydzielane na stosie. Oznacza to, że semantyka wartości jest prawie darmowa, a dostęp do elementów strukturalnych jest bardzo tani. To nie zapobiega polimorfizmowi.
Mogłabyś mieć każdą strukturę zaczynającą się od wskaźnika do swojej tablicy funkcji wirtualnych. Byłby to problem z wydajnością (każda struktura miałaby co najmniej rozmiar wskaźnika), ale jest to wykonalne. Pozwoliłoby to na funkcje wirtualne.
A co z dodawaniem pól?
Cóż, przydzielając strukturę na stosie, przydzielasz pewną ilość miejsca. Wymagane miejsce jest określane w czasie kompilacji (z wyprzedzeniem lub podczas JITtingu). Jeśli dodasz pola, a następnie przypiszesz do typu podstawowego:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
Spowoduje to nadpisanie nieznanej części stosu.
Alternatywą jest dla środowiska wykonawczego, aby temu zapobiec, zapisując tylko sizeof (A) bajtów do dowolnej zmiennej A.
Co się stanie, jeśli B przesłoni metodę w A i odwołuje się do jej pola Integer2? Środowisko uruchomieniowe zgłasza MemberAccessException lub zamiast tego metoda uzyskuje dostęp do niektórych losowych danych na stosie. Żadne z nich nie jest dopuszczalne.
Dziedziczenie struktur jest całkowicie bezpieczne, o ile nie używasz struktur polimorficznie lub nie dodajesz pól podczas dziedziczenia. Ale nie są one zbyt przydatne.
Wydaje się, że to bardzo częste pytanie. Mam ochotę dodać, że typy wartości są przechowywane „w miejscu”, w którym deklarujesz zmienną; Oprócz szczegółów implementacji oznacza to, że nie ma nagłówka obiektu, który mówi coś o obiekcie, tylko zmienna wie, jakie dane tam się znajdują.
Struktury obsługują interfejsy, więc możesz w ten sposób robić pewne rzeczy polimorficzne.
IL jest językiem opartym na stosie, więc wywołanie metody z argumentem wygląda mniej więcej tak:
Po uruchomieniu metody zdejmuje ze stosu kilka bajtów, aby pobrać argument. Wie dokładnie ile bajtów ma wyskoczyć, ponieważ argument jest albo wskaźnikiem typu referencyjnego (zawsze 4 bajty na 32-bitach), albo typem wartości, dla którego rozmiar jest zawsze dokładnie znany.
Jeśli jest to wskaźnik typu referencyjnego, metoda wyszukuje obiekt w stercie i pobiera jego uchwyt typu, który wskazuje na tabelę metod, która obsługuje tę konkretną metodę dla tego dokładnego typu. Jeśli jest to typ wartości, nie jest konieczne wyszukiwanie w tabeli metod, ponieważ typy wartości nie obsługują dziedziczenia, więc istnieje tylko jedna możliwa kombinacja metoda / typ.
Gdyby typy wartości obsługiwały dziedziczenie, wystąpiłoby dodatkowe obciążenie w tym, że konkretny typ struktury musiałby zostać umieszczony na stosie, a także jego wartość, co oznaczałoby pewnego rodzaju wyszukiwanie w tabeli metod dla konkretnego konkretnego wystąpienia typu. Pozwoliłoby to wyeliminować zalety szybkości i wydajności typów wartości.