C # nie zezwala strukturom na pochodzenie z klas, ale wszystkie ValueTypes pochodzą z Object. Gdzie dokonuje się tego rozróżnienia?
Jak CLR sobie z tym radzi?
c#
.net
clr
value-type
reference-type
Joan Venge
źródło
źródło
System.ValueType
czcionek w systemie typów CLR.Odpowiedzi:
Twoje stwierdzenie jest nieprawidłowe, stąd twoje zamieszanie. C # nie pozwalają kodowanym czerpać z klas. Wszystkie struktury pochodzą z tej samej klasy System.ValueType, która pochodzi od System.Object. Wszystkie wyliczenia pochodzą od System.Enum.
AKTUALIZACJA: W niektórych (obecnie usuniętych) komentarzach było pewne zamieszanie, które wymaga wyjaśnienia. Zadam dodatkowe pytania:
Oczywiście tak. Widzimy to czytając pierwszą stronę specyfikacji:
Teraz zauważam, że specyfikacja wyolbrzymia tutaj przypadek. Typy wskaźników nie pochodzą z obiektu, a relacja wyprowadzenia dla typów interfejsów i typów parametrów typu jest bardziej złożona, niż wskazuje ten szkic. Jednak oczywiście jest tak, że wszystkie typy struktur wywodzą się z typu podstawowego.
Pewnie. Typ struktury może przesłonić
ToString
. Co zastępuje, jeśli nie jest metodą wirtualną swojego typu podstawowego? Dlatego musi mieć typ podstawowy. Ten typ bazowy to klasa.Oczywiście nie. Nie oznacza to, że struktury nie pochodzą z klasy . Struktury wywodzą się z klasy iw ten sposób dziedziczą dziedziczone elementy tej klasy. W rzeczywistości struktury muszą pochodzić z określonej klasy: wyliczenia są wymagane, aby pochodzić z
Enum
, struktury muszą pochodzić zValueType
. Ponieważ są one wymagane , język C # zabrania określania relacji wyprowadzania w kodzie.Gdy wymagana jest relacja , projektant języka ma opcje: (1) wymagaj od użytkownika wpisania wymaganej inkantacji, (2) uczyń ją opcjonalną lub (3) zabron. Każdy z nich ma zalety i wady, a projektanci języka C # wybrali inaczej w zależności od konkretnych szczegółów każdego z nich.
Na przykład pola const muszą być statyczne, ale nie wolno mówić, że tak jest, ponieważ po pierwsze jest to bezcelowe rozwlekanie, a po drugie, oznacza, że istnieją niestatyczne pola stałe. Jednak przeciążone operatory muszą być oznaczone jako statyczne, mimo że deweloper nie ma wyboru; programistom zbyt łatwo jest uwierzyć, że w przeciwnym razie przeciążenie operatora jest metodą instancji. Eliminuje to obawę, że użytkownik może uwierzyć, że „statyczny” oznacza, że „wirtualny” jest również możliwy.
W tym przypadku wymaganie od użytkownika, aby powiedział, że jego struktura wywodzi się z ValueType, wydaje się zwykłym nadmiarem słownictwa, a to oznacza, że struktura może pochodzić z innego typu. Aby wyeliminować oba te problemy, C # sprawia, że stwierdzenie w kodzie, że struktura pochodzi od typu podstawowego, jest nielegalne , chociaż oczywiście tak jest.
Podobnie wszystkie typy delegatów pochodzą z
MulticastDelegate
, ale C # wymaga, abyś tego nie mówił.Więc teraz ustaliliśmy, że wszystkie struktury w C # pochodzą z klasy .
Wiele osób jest zdezorientowanych relacją dziedziczenia w języku C #. Relacja dziedziczenia jest dość prosta: jeśli typ struktury, klasy lub delegata D wywodzi się z klasy B, wówczas dziedziczone elementy członkowskie B są również członkami D. To takie proste.
Co to oznacza w odniesieniu do dziedziczenia, gdy mówimy, że struktura pochodzi od ValueType? Po prostu wszyscy dziedziczni członkowie ValueType są również członkami struktury. W ten sposób struktury uzyskują implementację
ToString
, na przykład; jest dziedziczony z klasy bazowej struktury.Tak. Wszyscy prywatni członkowie klasy bazowej są również członkami typu pochodnego. Oczywiście, dzwonienie do tych członków po imieniu jest nielegalne, jeśli strona wywoławcza nie znajduje się w domenie dostępności członka. To, że masz członka, nie oznacza, że możesz go używać!
Kontynuujemy teraz pierwotną odpowiedź:
Bardzo dobrze. :-)
To, co sprawia, że typ wartości jest typem wartości, polega na tym, że jego wystąpienia są kopiowane przez wartość . To, co sprawia, że typ referencyjny jest typem referencyjnym, polega na tym, że jego wystąpienia są kopiowane przez odwołanie . Wydaje się, że masz pewne przekonanie, że związek dziedziczenia między typami wartości i typami odniesienia jest w jakiś sposób wyjątkowy i niezwykły, ale nie rozumiem, czym jest to przekonanie. Dziedziczenie nie ma nic wspólnego ze sposobem kopiowania rzeczy.
Spójrz na to w ten sposób. Załóżmy, że przedstawiłem następujące fakty:
Istnieją dwa rodzaje pudełek, czerwone i niebieskie.
Każde czerwone pudełko jest puste.
Istnieją trzy specjalne niebieskie pola zwane O, V i E.
O nie ma w żadnym pudełku.
V jest wewnątrz O.
E jest wewnątrz V.
Żadne inne niebieskie pudełko nie znajduje się w V.
Wewnątrz E. nie ma niebieskiego pudełka.
Każde czerwone pole jest w V lub E.
Każde niebieskie pudełko inne niż O znajduje się w niebieskim pudełku.
Niebieskie pola to typy referencyjne, czerwone pola to typy wartości, O to System.Object, V to System.ValueType, E to System.Enum, a relacja „wewnętrzna” to „pochodzi z”.
To doskonale spójny i prosty zestaw reguł, które możesz łatwo wdrożyć samodzielnie, mając dużo kartonu i dużo cierpliwości. To, czy pudełko jest czerwone czy niebieskie, nie ma nic wspólnego z tym, co jest w środku; w prawdziwym świecie całkowicie możliwe jest umieszczenie czerwonego pudełka w niebieskim pudełku. W środowisku CLR jest całkowicie legalne utworzenie typu wartości, który dziedziczy po typie referencyjnym, o ile jest to System.ValueType lub System.Enum.
Więc przeformułujmy twoje pytanie:
tak jak
Kiedy tak to wyrażasz, mam nadzieję, że to oczywiste. Nic nie stoi na przeszkodzie, aby umieścić czerwone pudełko w pudełku V, które znajduje się w pudełku O, które jest niebieskie. Dlaczego miałoby być?
DODATKOWA AKTUALIZACJA:
Pierwotne pytanie Joan dotyczyło tego, jak to jest możliweże typ wartości pochodzi od typu referencyjnego. Moja pierwotna odpowiedź tak naprawdę nie wyjaśniła żadnego z mechanizmów używanych przez środowisko CLR do wyjaśnienia faktu, że mamy relację wyprowadzania między dwiema rzeczami, które mają zupełnie różne reprezentacje - a mianowicie, czy dane, do których się odwołujemy, mają nagłówek obiektu, sync block, niezależnie od tego, czy jest właścicielem własnego magazynu na potrzeby czyszczenia pamięci i tak dalej. Mechanizmy te są skomplikowane, zbyt skomplikowane, by można je było wyjaśnić w jednej odpowiedzi. Zasady systemu typów CLR są nieco bardziej złożone niż nieco uproszczony jego smak, który widzimy na przykład w C #, gdzie nie ma wyraźnego rozróżnienia między wersjami pudełkowymi i nieopakowanymi. Wprowadzenie leków generycznych spowodowało również dodanie dużej złożoności do CLR.
źródło
Niewielka poprawka, C # nie pozwala strukturom na niestandardowe pochodzenie z niczego, nie tylko z klas. Wszystko, co może zrobić struktura, to zaimplementować interfejs, który bardzo różni się od wyprowadzenia.
Myślę, że najlepszym sposobem odpowiedzi na to jest to, że
ValueType
jest wyjątkowy. Zasadniczo jest to klasa bazowa dla wszystkich typów wartości w systemie typów CLR. Trudno wiedzieć, jak odpowiedzieć „jak CLR sobie z tym radzi”, ponieważ jest to po prostu reguła CLR.źródło
ValueType
jest to wyjątkowe, ale warto wyraźnie wspomnieć, żeValueType
sam w sobie jest typem referencyjnym.Jest to nieco sztuczna konstrukcja obsługiwana przez środowisko CLR, aby umożliwić traktowanie wszystkich typów jako System.Object.
Typy wartości pochodzą od System.Object do System.ValueType , czyli tam, gdzie występuje specjalna obsługa (tj .: środowisko CLR obsługuje pakowanie / rozpakowywanie itp. Dla dowolnego typu pochodzącego z ValueType).
źródło
Więc spróbujmy tego:
struct MyStruct : System.ValueType { }
To się nawet nie skompiluje. Kompilator przypomni Ci, że „Typ„ System.ValueType ”na liście interfejsów nie jest interfejsem”.
Podczas dekompilacji struktury Int32, która jest strukturą, znajdziesz:
public struct Int32: IComparable, IFormattable, IConvertible {}, nie wspominając o tym, że pochodzi z System.ValueType. Ale w przeglądarce obiektów widać, że Int32 dziedziczy po System.ValueType.
Wszystko to prowadzi mnie do wiary:
źródło
ValueType
, używa jej do zdefiniowania dwóch rodzajów obiektów: typu obiektu sterty, który zachowuje się jak typ referencyjny i typ miejsca przechowywania, który faktycznie znajduje się poza systemem dziedziczenia typów. Ponieważ te dwa rodzaje rzeczy są używane w wzajemnie wykluczających się kontekstach, te same deskryptory typów mogą być używane do obu. Na poziomie CLR struktura jest definiowana jako klasa, której rodzicem jestSystem.ValueType
, ale C # ...System.ValueType
), i zabrania klasom określania, że dziedziczą z,System.ValueType
ponieważ każda klasa, która została w ten sposób zadeklarowana, zachowywałaby się jak typ wartości.Typ wartości w ramce jest w rzeczywistości typem referencyjnym (chodzi jak jeden i kwacze jak jeden, więc skutecznie jest jednym). Sugerowałbym, że ValueType nie jest w rzeczywistości typem podstawowym typów wartości, ale raczej jest podstawowym typem odniesienia, do którego typy wartości mogą być konwertowane podczas rzutowania na typ Object. Same typy wartości bez pudełek znajdują się poza hierarchią obiektów.
źródło
Racjonalne uzasadnienie
Ze wszystkich odpowiedzi odpowiedź @ supercat jest najbliższa rzeczywistej odpowiedzi. Ponieważ inne odpowiedzi tak naprawdę nie odpowiadają na pytanie i wręcz zawierają nieprawidłowe twierdzenia (na przykład, że typy wartości dziedziczą po wszystkim), postanowiłem odpowiedzieć na to pytanie.
Prolog
Ta odpowiedź jest oparta na mojej własnej inżynierii wstecznej i specyfikacji CLI.
struct
iclass
są słowami kluczowymi C #. Jeśli chodzi o CLI, wszystkie typy (klasy, interfejsy, struktury itp.) Są zdefiniowane w definicjach klas.Na przykład typ obiektu (znany w C # jako
class
) jest zdefiniowany w następujący sposób:.class MyClass { }
Interfejs jest definiowany przez definicję klasy z
interface
atrybutem semantycznym:.class interface MyInterface { }
A co z typami wartości?
Powodem, dla którego struktury mogą dziedziczyć
System.ValueType
i nadal być typami wartości, jest to, że ... tak nie jest.Typy wartości to proste struktury danych. Typy wartości nie dziedziczą z niczego i nie mogą implementować interfejsów. Typy wartości nie są podtypami żadnego typu i nie zawierają żadnych informacji o typie. Biorąc pod uwagę adres pamięci typu wartości, nie można zidentyfikować, co reprezentuje typ wartości, w przeciwieństwie do typu referencyjnego, który zawiera informacje o typie w ukrytym polu.
Jeśli wyobrazimy sobie następującą strukturę C #:
namespace MyNamespace { struct MyValueType : ICloneable { public int A; public int B; public int C; public object Clone() { // body omitted } } }
Poniżej znajduje się definicja klasy IL tej struktury:
.class MyNamespace.MyValueType extends [mscorlib]System.ValueType implements [mscorlib]System.ICloneable { .field public int32 A; .field public int32 B; .field public int32 C; .method public final hidebysig newslot virtual instance object Clone() cil managed { // body omitted } }
Więc co się tutaj dzieje? Wyraźnie rozszerza
System.ValueType
, który jest typem obiektowym / referencyjnym, i implementujeSystem.ICloneable
.Wyjaśnienie jest takie, że kiedy definicja klasy się rozszerza
System.ValueType
, w rzeczywistości definiuje dwie rzeczy: typ wartości i odpowiadający mu typ pudełkowy. Elementy członkowskie definicji klasy definiują reprezentację zarówno typu wartości, jak i odpowiedniego typu pudełkowego. Nie jest to typ wartości, który rozszerza i implementuje, jest to odpowiedni typ pudełkowy.extends
iimplements
słowa kluczowe odnoszą się tylko do wersji pudełkowej typu.Aby wyjaśnić, powyższa definicja klasy ma 2 rzeczy:
System.ValueType
i implementującSystem.ICloneable
interfejs.Należy również zauważyć, że każde rozszerzenie definicji klasy
System.ValueType
jest również wewnętrznie zapieczętowane, niezależnie od tego, czysealed
słowo kluczowe jest określone, czy nie.Ponieważ typy wartości to tylko proste struktury, nie dziedziczą, nie implementują i nie obsługują polimorfizmu, nie można ich używać z resztą systemu typów. Aby obejść ten problem, oprócz typu wartości środowisko CLR definiuje również odpowiedni typ odwołania z tymi samymi polami, zwany typem pudełkowym. Więc gdy typ wartości nie mogą być przekazywane wokół metod biorących
object
, odpowiadającej jej pudełkowej typu puszki .Teraz, gdybyś miał zdefiniować metodę w C # jak
public static void BlaBla(MyNamespace.MyValueType x)
,wiesz, że metoda przyjmie typ wartości
MyNamespace.MyValueType
.Powyżej dowiedzieliśmy się, że definicja klasy, która wynika ze
struct
słowa kluczowego w języku C #, w rzeczywistości definiuje zarówno typ wartości, jak i typ obiektu. Możemy jednak odnosić się tylko do zdefiniowanego typu wartości. Mimo że specyfikacja CLI stwierdza, że słowo kluczowe constraintboxed
może być użyte do odniesienia się do pudełkowej wersji typu, to słowo kluczowe nie istnieje (patrz ECMA-335, II.13.1 Odwoływanie się do typów wartości). Ale wyobraźmy sobie przez chwilę, że tak.W przypadku odwoływania się do typów w języku IL obsługiwanych jest kilka ograniczeń, wśród których są
class
ivaluetype
. Jeśli używamy,valuetype MyNamespace.MyType
określamy definicję klasy typu wartości o nazwie MyNamespace.MyType. Podobnie możemy użyćclass MyNamespace.MyType
do określenia definicji klasy obiektu o nazwie MyNamespace.MyType. Oznacza to, że w IL możesz mieć typ wartości (struct) i typ obiektu (klasa) o tej samej nazwie i nadal je rozróżniać. Teraz, gdybyboxed
słowo kluczowe wskazane w specyfikacji CLI zostało faktycznie zaimplementowane, moglibyśmy użyćboxed MyNamespace.MyType
do określenia typu pudełkowego definicji klasy wartości o nazwie MyNamespace.MyType.Tak więc
.method static void Print(valuetype MyNamespace.MyType test) cil managed
przyjmuje typ wartości zdefiniowany przez definicję klasy typu o nazwieMyNamespace.MyType
,while
.method static void Print(class MyNamespace.MyType test) cil managed
przyjmuje typ obiektu zdefiniowany przez definicję klasy typu obiektu o nazwieMyNamespace.MyType
.podobnie gdyby
boxed
było słowem kluczowym,.method static void Print(boxed MyNamespace.MyType test) cil managed
przyjąłby typ pudełkowy typu wartości zdefiniowanego przez definicję klasy o nazwieMyNamespace.MyType
.Można by następnie móc instancji pudełkowej typ jak każdy inny rodzaj obiektu i przekazać ją wokół każdej metody, które wykonuje
System.ValueType
,object
lubboxed MyNamespace.MyValueType
jako argument, i to będzie dla wszystkich zamiarów i celów, pracy jak każdy inny rodzaj odniesienia. NIE jest to typ wartości, ale odpowiadający mu typ pudełkowy typu wartości.Podsumowanie
Podsumowując, i odpowiadając na pytanie:
Typy wartości nie są typami odwołań i nie dziedziczą
System.ValueType
ani z żadnego innego typu, ani nie mogą implementować interfejsów. Odpowiednie typy pudełkowe , które również są zdefiniowane , dziedzicząSystem.ValueType
i mogą implementować interfejsy..class
Definicja określa różne rzeczy w zależności od okoliczności.interface
atrybut semantyczny, definicja klasy definiuje interfejs.interface
atrybut semantyczny nie jest określony, a definicja się nie rozszerzaSystem.ValueType
, definicja klasy definiuje typ obiektu (klasę).interface
atrybut semantyczny nie jest określony, a definicja się rozszerzaSystem.ValueType
, definicja klasy definiuje typ wartości i odpowiadający mu typ pudełkowy (struct).Układ pamięci
W tej sekcji przyjęto proces 32-bitowy
Jak już wspomniano, typy wartości nie zawierają informacji o typie, a zatem nie można zidentyfikować, co reprezentuje typ wartości na podstawie jego lokalizacji w pamięci. Struktura opisuje prosty typ danych i zawiera tylko pola, które definiuje:
public struct MyStruct { public int A; public short B; public int C; }
Jeśli wyobrazimy sobie, że instancja MyStruct została przydzielona pod adresem 0x1000, to jest to układ pamięci:
0x1000: int A; 0x1004: short B; 0x1006: 2 byte padding 0x1008: int C;
Struktury domyślnie mają układ sekwencyjny. Pola są wyrównane do granic własnego rozmiaru. Aby to spełnić, dodano wypełnienie.
Jeśli zdefiniujemy klasę dokładnie w ten sam sposób, jak:
public class MyClass { public int A; public short B; public int C; }
Wyobrażając sobie ten sam adres, układ pamięci wygląda następująco:
0x1000: Pointer to object header 0x1004: int A; 0x1008: int C; 0x100C: short B; 0x100E: 2 byte padding 0x1010: 4 bytes extra
Klasy mają domyślnie układ automatyczny, a kompilator JIT ułoży je w najbardziej optymalnej kolejności. Pola są wyrównane do granic własnego rozmiaru. Aby to spełnić, dodano wypełnienie. Nie wiem dlaczego, ale każda klasa ma zawsze na końcu dodatkowe 4 bajty.
Przesunięcie 0 zawiera adres nagłówka obiektu, który zawiera informacje o typie, wirtualną tabelę metod itp. Pozwala to środowisku wykonawczemu zidentyfikować, co reprezentują dane pod adresem, w przeciwieństwie do typów wartości.
Dlatego typy wartości nie obsługują dziedziczenia, interfejsów ani polimorfizmu.
Metody
Typy wartości nie mają tabel metod wirtualnych, a zatem nie obsługują polimorfizmu. Jednak ich odpowiedni typ pudełkowy tak .
Kiedy masz instancję struktury i próbujesz wywołać metodę wirtualną, taką jak
ToString()
zdefiniowano wSystem.Object
, środowisko wykonawcze musi opakować strukturę.MyStruct myStruct = new MyStruct(); Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
Jednakże, jeśli struktura nadpisuje,
ToString()
wówczas wywołanie będzie statycznie związane, a środowisko uruchomieniowe będzie wywoływaćMyStruct.ToString()
bez pakowania i bez zaglądania do żadnych tabel metod wirtualnych (struktury nie mają żadnych). Z tego powodu jest również w stanie wbudowaćToString()
połączenie.Jeśli struktura zastępuje
ToString()
i jest zapakowana, wywołanie zostanie rozwiązane przy użyciu tabeli metod wirtualnych.System.ValueType myStruct = new MyStruct(); // Creates a new instance of the boxed type of MyStruct. Console.WriteLine(myStruct.ToString()); // ToString() is now called through the virtual method table.
Pamiętaj jednak, że
ToString()
jest to zdefiniowane w strukturze, a zatem działa na wartości struktury, więc oczekuje typu wartości. Typ pudełkowy, jak każda inna klasa, ma nagłówek obiektu. GdybyToString()
metoda zdefiniowana w strukturze została wywołana bezpośrednio z typem pudełkowym wethis
wskaźniku, przy próbie dostępu do polaA
wMyStruct
, uzyskałaby dostęp do offsetu 0, który w typie pudełkowym byłby wskaźnikiem nagłówka obiektu. Tak więc typ pudełkowy ma ukrytą metodę, która faktycznie zastępujeToString()
. Ta ukryta metoda odpakowuje (tylko obliczanie adresu, podobnie jakunbox
instrukcja IL) typ pudełkowy, a następnie statycznie wywołujeToString()
zdefiniowany w strukturze.Podobnie, typ pudełkowy ma ukrytą metodę dla każdej zaimplementowanej metody interfejsu, która wykonuje to samo rozpakowywanie, a następnie statycznie wywołuje metodę zdefiniowaną w strukturze.
Specyfikacja CLI
Boks
Definiowanie typów wartości
Typy wartości nie dziedziczą
Typy wartości nie implementują interfejsów
Nieistniejące słowo kluczowe w pudełku
Uwaga: tutaj specyfikacja jest nieprawidłowa, nie ma
boxed
słowa kluczowego.Epilog
Myślę, że część zamieszania związanego z tym, jak typy wartości wydają się dziedziczyć, wynika z faktu, że C # używa składni rzutowania do wykonywania pudełek i rozpakowywania, co sprawia, że wygląda na to, że wykonujesz rzutowania, co tak naprawdę nie ma miejsca (chociaż CLR zgłosi InvalidCastException w przypadku próby rozpakowania niewłaściwego typu).
(object)myStruct
w C # tworzy nowe wystąpienie typu pudełkowego typu wartości; nie wykonuje żadnych rzutów. Podobnie,(MyStruct)obj
w C # unboxing a boxed type, copy the value part out; nie wykonuje żadnych rzutów.źródło