Dlaczego struktury nie obsługują dziedziczenia?

128

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?

Julia
źródło
6
Nie umieram z powodu tej funkcji, ale przychodzi mi do głowy kilka przypadków, w których dziedziczenie struktur byłoby przydatne: możesz chcieć rozszerzyć strukturę Point2D na strukturę Point3D z dziedziczeniem, możesz chcieć dziedziczyć z Int32, aby ograniczyć jej wartości od 1 do 100, możesz chcieć utworzyć typ-def, który jest widoczny w wielu plikach (sztuczka Using typeA = typeB ma tylko zakres plików) itd.
Juliet
4
Możesz przeczytać stackoverflow.com/questions/1082311/… , który wyjaśnia nieco więcej o strukturach i dlaczego powinny być ograniczone do określonego rozmiaru. Jeśli chcesz używać dziedziczenia w strukturze, prawdopodobnie powinieneś używać klasy.
Justin,
1
A może zechcesz przeczytać stackoverflow.com/questions/1222935/ ... ponieważ dogłębnie dowiesz się, dlaczego nie można tego zrobić na platformie dotNet. Zrobili to na sposób C ++, z tymi samymi problemami, które mogą być katastrofalne dla zarządzanej platformy.
Dykam
@Justin Classes ma koszty wydajności, których struktury mogą uniknąć. A w tworzeniu gier to naprawdę ma znaczenie. Dlatego w niektórych przypadkach nie powinieneś używać klasy, jeśli możesz temu pomóc.
Gavin Williams
@Dykam Myślę, że można to zrobić w C #. Katastrofalne to przesada. Mogę dziś napisać katastrofalny kod w języku C #, gdy nie jestem zaznajomiony z techniką. Więc to nie jest problem. Jeśli dziedziczenie struktur może rozwiązać niektóre problemy i zapewnić lepszą wydajność w pewnych sytuacjach, to jestem za tym.
Gavin Williams

Odpowiedzi:

121

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 FooTypejest typem referencyjnym, na stercie zarządzanym zostanie utworzonych 11 obiektów (jeden dla tablicy i 10 dla każdego wystąpienia typu). Jeśli FooTypejest 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 Basei Derivedsą typami wartości, a tablice przechowują wartości w linii, to mamy problem.

Mamy problem, ponieważ Square()nic o Derivedtym 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*=2by rzeczywiście być modyfikujący values[0].B!

Spróbuj debugowania TEGO !

jonp
źródło
11
Rozsądnym rozwiązaniem tego problemu byłoby zablokowanie rzutowanej postaci Base [] do Detived []. Podobnie jak rzutowanie z short [] na int [] jest zabronione, chociaż możliwe jest przesyłanie z short na int.
Niki,
3
+ odpowiedź: problem z dziedziczeniem nie pojawił się u mnie, dopóki nie ułożysz go w kategoriach tablic. Inny użytkownik stwierdził, że ten problem można złagodzić przez „krojenie” struktur do odpowiedniego rozmiaru, ale według mnie cięcie jest przyczyną większej liczby problemów, niż rozwiązuje.
Julia,
4
Tak, ale „ma to sens”, ponieważ konwersje tablic dotyczą niejawnych konwersji, a nie jawnych konwersji. short to int jest możliwe, ale wymaga rzutowania, więc rozsądne jest, że short [] nie może zostać przekonwertowane na int [] (bez kodu konwersji, np. 'a.Select (x => (int) x) .ToArray ( ) '). Jeśli środowisko wykonawcze nie zezwoli na rzutowanie z Base do Derived, będzie to "brodawka", ponieważ JEST to dozwolone dla typów referencyjnych. Mamy więc możliwe dwie różne „brodawki” - zabronić dziedziczenia struktury lub zabronić konwersji tablic-wyprowadzonych na tablice-bazy.
jonp
3
Przynajmniej zapobiegając dziedziczeniu struktur, mamy oddzielne słowo kluczowe i możemy łatwiej powiedzieć „struktury są specjalne”, w przeciwieństwie do „losowych” ograniczeń w czymś, co działa dla jednego zestawu rzeczy (klas), ale nie dla innego (struktur) . Wyobrażam sobie, że ograniczenie struktury jest znacznie łatwiejsze do wyjaśnienia („są różne!”).
jonp
2
potrzeba, aby zmienić nazwę funkcji z „placu” na „double”
Jan
69

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?
Kenan EK
źródło
5
C ++ odpowiedział na to, wprowadzając pojęcie „krojenia”, więc jest to problem do rozwiązania. Dlaczego więc nie powinno być obsługiwane dziedziczenie struktur?
jonp
1
Rozważ tablice dziedzicznych struktur i pamiętaj, że C # jest językiem zarządzanym (w pamięci). Cięcie na plasterki lub jakakolwiek podobna opcja siałaby spustoszenie w podstawach CLR.
Kenan EK
4
@jonp: Można rozwiązać, tak. Pożądany? Oto eksperyment myślowy: wyobraź sobie, że masz klasę bazową Vector2D (x, y) i klasę pochodną Vector3D (x, y, z). Obie klasy mają właściwość Magnitude, która oblicza odpowiednio sqrt (x ^ 2 + y ^ 2) i sqrt (x ^ 2 + y ^ 2 + z ^ 2). Jeśli napiszesz „Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', co powinno zwrócić' a.Magnitude == b.Magnitude '? Jeśli następnie napiszemy 'a = (Vector3D) b', czy a.Magnitude ma taką samą wartość przed przypisaniem, jak po? Projektanci .NET zapewne powiedzieli sobie: „nie, nie będziemy tego mieli”.
Julia,
1
To, że problem można rozwiązać, nie oznacza, że ​​należy go rozwiązać. Czasami najlepiej jest unikać sytuacji, w których pojawia się problem.
Dan Diplo,
@ kek444: Posiadanie Foodziedziczenia struktury Barnie powinno pozwalać Foona 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 Barjako pierwszy element Fooi Foouwzględnij nazwy członków, że alias do tych członków Bar, pozwalając kodu, które wykorzystywane Barbyć dostosowana do użyć Foozamiast tego, bez konieczności wymiany wszystkich odniesień do thing.BarMemberz thing.theBar.BarMember, i zachowując możliwość odczytu i zapisu wszystkich Bar„s dziedzinach jak grupy; ...
supercat
14

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.

Martin Liversage
źródło
8
nie trzeba używać polimorfizmu, aby skorzystać z dziedziczenia
rmeador
Więc masz ile różnych typów dziedziczenia w .NET?
John Saunders,
Polimorfizm istnieje w strukturach, wystarczy wziąć pod uwagę różnicę między wywołaniem ToString () podczas implementacji w niestandardowej strukturze lub gdy niestandardowa implementacja ToString () nie istnieje.
Kenan EK
Dzieje się tak, ponieważ wszystkie pochodzą od System.Object. To bardziej polimorfizm typu System.Object niż struktur.
John Saunders
Polimorfizm może mieć znaczenie w przypadku struktur używanych jako parametry typu ogólnego. Polimorfizm działa ze strukturami implementującymi interfejsy; największym problemem związanym z interfejsami jest to, że nie mogą one ujawniać byrefów w polach struktur. W przeciwnym razie największą rzeczą, o której myślę, byłaby pomocna, o ile „dziedziczenie” struktur byłoby sposobem na posiadanie typu (struktury lub klasy), Fooktóry ma pole typu struktury, Barktóry mógłby traktować Barelementy członkowskie jako własne, tak aby Point3dklasa mogła np hermetyzacji Point2d xyale odnoszą się do Xtej dziedzinie, jak też xy.Xczy X.
supercat
8

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 structodziedziczyli 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ą.

Blixt
źródło
4
To nie jest powód, dla którego nie ma spadku.
Dykam
Uważam, że dziedziczenie, o którym tutaj mowa, nie polega na możliwości użycia dwóch struktur, w których jedna dziedziczy po drugiej zamiennie, ale ponowne wykorzystanie i dodanie do implementacji jednej struktury do drugiej (tj. Utworzenie Point3Dz a Point2D; nie byłbyś w stanie aby użyć a Point3Dzamiast a Point2D, ale nie musiałbyś ponownie implementować Point3Dcałkowicie od zera.) Tak i tak to zinterpretowałem ...
Blixt
Krótko mówiąc: może wspierać dziedziczenie bez polimorfizmu. Tak nie jest. Wierzę, że jest to wybór projektu, aby pomóc osoba wybrać classsię structw razie potrzeby.
Blixt
3

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.

Dykam
źródło
2
C ++ radzi sobie dobrze z tym przypadkiem, IIRC. Wystąpienie B jest dzielone tak, aby pasowało do rozmiaru A. Jeśli jest to czysty typ danych, tak jak struktury .NET, nic złego się nie stanie. Występuje pewien problem z metodą, która zwraca A i przechowujesz tę zwracaną wartość w B, ale nie powinno to być dozwolone. Krótko mówiąc, projektanci .NET mogliby sobie z tym poradzić, gdyby chcieli, ale nie zrobili tego z jakiegoś powodu.
rmeador
1
W przypadku Twojej DoSomething () prawdopodobnie nie będzie problemu, ponieważ (zakładając semantykę C ++) 'b' byłoby „podzielone” w celu utworzenia instancji A. Problem dotyczy <i> tablic </i>. Rozważ istniejące struktury A i B oraz metodę <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Ponieważ tablice typów wartości przechowują wartości „inline”, DoSomething (current = new B [2] {}) spowoduje ustawienie rzeczywistej właściwości [0] .childproperty, a nie rzeczywistej [1] .property. To jest złe.
jonp
2
@John: Nie twierdziłem, że tak, i nie sądzę, że @jonp też. Wspomnieliśmy tylko, że ten problem jest stary i został rozwiązany, więc projektanci .NET postanowili nie wspierać go z innego powodu niż niemożność techniczna.
rmeador
Należy zauważyć, że kwestia „tablic typów pochodnych” nie jest nowością w C ++; zobacz parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Tablice w C ++ są złe! ;-)
jonp
@John: rozwiązaniem problemu „tablice typów pochodnych i typów bazowych nie mieszają się” jest, jak zwykle, nie rób tego. Dlatego tablice w C ++ są złe (łatwiej dopuszczają uszkodzenie pamięci) i dlaczego .NET nie obsługuje dziedziczenia z typami wartości (kompilator i JIT zapewniają, że tak się nie stanie).
jonp
3

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ć. :-)

Rui Craveiro
źródło
3

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.

user38001
źródło
1
Prawie. Nikt inny nie wspomniał o problemie krojenia w odniesieniu do stosu, tylko w odniesieniu do tablic. I nikt inny nie wspomniał o dostępnych rozwiązaniach.
user38001
1
Wszystkie typy wartości w .net są wypełniane zerami przy tworzeniu, niezależnie od ich typu lub pól, które zawierają. Dodanie czegoś takiego jak wskaźnik vtable do struktury wymagałoby zainicjowania typów z niezerowymi wartościami domyślnymi. Taka funkcja może być przydatna do różnych celów, a jej implementacja w większości przypadków może nie być zbyt trudna, ale w .net nic takiego nie istnieje.
supercat
@ user38001 "Struktury są przydzielane na stosie" - chyba że są polami instancji, w którym to przypadku są przydzielane na stercie.
David Klempfner
2

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ą.

Cecil ma imię
źródło
Kompilator wie, co tam jest. Odwoływanie się do C ++ nie może być odpowiedzią.
Henk Holterman
Skąd wywnioskowałeś C ++? Powiedziałbym, że w miejscu, ponieważ to właśnie najlepiej pasuje do zachowania, stos jest szczegółem implementacji, cytując artykuł na blogu MSDN.
Cecil ma imię
Tak, wspominanie o C ++ było złe, tylko mój tok myślenia. Ale poza pytaniem, czy potrzebne są informacje o czasie wykonywania, dlaczego struktury nie miałyby mieć „nagłówka obiektu”? Kompilator może je połączyć tak, jak chce. Może nawet ukryć nagłówek w strukturze [Structlayout].
Henk Holterman,
Ponieważ struktury są typami wartości, nie jest to konieczne w przypadku nagłówka obiektu, ponieważ środowisko wykonawcze zawsze kopiuje zawartość, tak jak w przypadku innych typów wartości (ograniczenie). Nie miałoby to sensu z nagłówkiem, ponieważ do tego służą klasy referencyjne: P
Cecil ma imię
1

Struktury obsługują interfejsy, więc możesz w ten sposób robić pewne rzeczy polimorficzne.

Wyatt Barnett
źródło
0

IL jest językiem opartym na stosie, więc wywołanie metody z argumentem wygląda mniej więcej tak:

  1. Umieść argument na stosie
  2. Wywołaj metodę.

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.

Matt Howells
źródło
1
C ++ rozwiązał ten problem, przeczytaj tę odpowiedź, aby
poznać