W jaki sposób ValueTypes wywodzą się z Object (ReferenceType) i nadal są ValueTypes?

83

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?

Joan Venge
źródło
Wynik czarnej magii System.ValueTypeczcionek w systemie typów CLR.
RBT

Odpowiedzi:

108

C # nie zezwala strukturom na pochodzenie z klas

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:

Czy struktury pochodzą z typu podstawowego?

Oczywiście tak. Widzimy to czytając pierwszą stronę specyfikacji:

Wszystkie typy C #, w tym typy pierwotne, takie jak int i double, dziedziczą z pojedynczego typu obiektu głównego.

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.

Czy istnieją inne sposoby, dzięki którym wiemy, że 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.

Czy mogę wyprowadzić strukturę zdefiniowaną przez użytkownika z wybranej przeze mnie klasy?

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ć z ValueType. Ponieważ są one wymagane , język C # zabrania określania relacji wyprowadzania w kodzie.

Dlaczego tego zabronić?

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 .

Jaki jest związek między dziedziczeniem a wyprowadzeniem 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.

Wszyscy dziedziczni członkowie? Na pewno nie. Czy członkowie prywatni są dziedziczeni?

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ź:


Jak CLR sobie z tym radzi?

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:

W jaki sposób ValueTypes wywodzą się z Object (ReferenceType) i nadal są ValueTypes?

tak jak

Jak to możliwe, że każda czerwona ramka (typy wartości) znajduje się wewnątrz (pochodzi z) ramki O (System.Object), która jest niebieską ramką (typ odniesienia) i nadal jest czerwoną ramką (typ wartości)?

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.

Eric Lippert
źródło
8
Konstrukcje językowe powinny mieć znaczenie. Co oznaczałoby posiadanie dowolnego typu wartości pochodzącego z dowolnego typu odniesienia? Czy jest coś, co można by osiągnąć za pomocą takiego schematu, czego nie można by było również osiągnąć za pomocą niejawnych konwersji zdefiniowanych przez użytkownika?
Eric Lippert
2
Nie sądzę. Pomyślałem tylko, że możesz mieć niektórych członków dostępnych dla wielu typów wartości, które widzisz jako grupę, co możesz zrobić, używając klasy abstrakcyjnej do wyprowadzenia struktury. Myślę, że mógłbyś użyć niejawnych konwersji, ale wtedy zapłaciłbyś za wydajność, prawda? Jeśli robisz ich miliony.
Joan Venge
8
O, rozumiem. Chcesz używać dziedziczenia nie jako mechanizmu modelowania „jest rodzajem” relacji, ale raczej jako mechanizmu udostępniania kodu między wieloma pokrewnymi typami. Wydaje się, że to rozsądny scenariusz, chociaż osobiście staram się unikać dziedziczenia wyłącznie jako ułatwienia w dzieleniu się kodem.
Eric Lippert
3
Joan, aby zdefiniować zachowanie raz, możesz utworzyć interfejs, mieć struktury, które chcesz udostępnić, zaimplementować interfejs, a następnie utworzyć metodę rozszerzenia działającą na interfejsie. Jednym z potencjalnych problemów z tym podejściem jest wywołanie metod interfejsu, w których struktura zostanie najpierw umieszczona w pudełku, a skopiowana wartość w pudełku zostanie przesłana do metody rozszerzającej. Każda zmiana stanu nastąpi na kopii obiektu, co może być nieintuicyjne dla użytkowników interfejsu API.
David Silva Smith
2
@Sipo: Teraz, żeby być uczciwym, pytanie zawiera „jak CLR sobie z tym radzi?” a odpowiedź dobrze się spisuje, opisując, w jaki sposób środowisko CLR implementuje te reguły. Ale chodzi o to: powinniśmy się spodziewać, że system implementujący język nie ma takich samych reguł jak język! Systemy wdrożeniowe są siłą rzeczy niższego poziomu, ale nie mylmy zasad tego systemu niższego poziomu z regułami systemu wysokiego poziomu, który jest na nim zbudowany. Jasne, system typów CLR rozróżnia typy wartości w pudełkach i bez pudełek, jak zauważyłem w mojej odpowiedzi. Ale C # tego nie robi .
Eric Lippert
21

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

JaredPar
źródło
+1 za dobry punkt na temat struktur, które nie pochodzą od niczego [z wyjątkiem niejawnie wywodzących się z System.ValueType].
Reed Copsey
2
Mówisz, że ValueTypejest to wyjątkowe, ale warto wyraźnie wspomnieć, że ValueTypesam w sobie jest typem referencyjnym.
LukeH
Jeśli wewnętrznie struktury mogą wywodzić się z klasy, dlaczego nie udostępniają jej wszystkim?
Joan Venge
1
@Joan: Tak naprawdę nie. Dzieje się tak tylko po to, abyś mógł rzucić strukturę na obiekt i tam na użyteczność. Ale technicznie, w porównaniu do sposobu implementacji klas, typy wartości są obsługiwane przez środowisko CLR zupełnie inaczej.
Reed Copsey
2
@JoanVenge Uważam, że zamieszanie polega na tym, że struktury pochodzą z klasy ValueType w środowisku CLR. Uważam, że bardziej poprawne jest stwierdzenie, że w środowisku CLR struktury tak naprawdę nie istnieją, a implementacja „struktury” w środowisku CLR jest w rzeczywistości klasą ValueType. Więc to nie jest tak, że struktura dziedziczy po ValueType w środowisku CLR.
wired_in
20

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

Reed Copsey
źródło
6

Twoje stwierdzenie jest nieprawidłowe, stąd twoje zamieszanie. C # zezwala strukturom na pochodzenie z klas. Wszystkie struktury pochodzą z tej samej klasy System.ValueType

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:

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.

yi.han
źródło
Te same struktury danych są używane w .NET do opisywania zawartości typów wartości i typów referencyjnych, ale gdy środowisko CLR widzi definicję typu, która jest zdefiniowana jako pochodzenie 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 jest System.ValueType, ale C # ...
supercat
... zabrania określania, że ​​struktury dziedziczą z czegokolwiek, ponieważ jest tylko jedna rzecz, którą mogą dziedziczyć po ( System.ValueType), i zabrania klasom określania, że ​​dziedziczą z, System.ValueTypeponieważ każda klasa, która została w ten sposób zadeklarowana, zachowywałaby się jak typ wartości.
supercat
3

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.

supercat
źródło
1
Myślę, że masz na myśli, że „ValueType nie jest tak naprawdę podstawowym typem typów wartości
wired_in
1
@wired_in: Dzięki. Poprawione.
supercat
2

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 i class 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 interfaceatrybutem semantycznym:

.class interface MyInterface
{
}

 

A co z typami wartości?

Powodem, dla którego struktury mogą dziedziczyć System.ValueTypei 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 implementuje System.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. extendsiimplements słowa kluczowe odnoszą się tylko do wersji pudełkowej typu.

Aby wyjaśnić, powyższa definicja klasy ma 2 rzeczy:

  1. Definiuje typ wartości z 3 polami (i jedną metodą). Nie dziedziczy po niczym i nie implementuje żadnych interfejsów (typy wartości też nie mogą).
  2. Definiuje typ obiektu (typ pudełkowy) z 3 polami (i implementuje jedną metodę interfejsu), dziedzicząc z System.ValueTypei implementując System.ICloneableinterfejs.

Należy również zauważyć, że każde rozszerzenie definicji klasy System.ValueTypejest 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 structsł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 constraint boxedmoż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ą classi valuetype. Jeśli używamy, valuetype MyNamespace.MyTypeokreślamy definicję klasy typu wartości o nazwie MyNamespace.MyType. Podobnie możemy użyć class MyNamespace.MyTypedo 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, gdyby boxedsł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 managedprzyjmuje typ wartości zdefiniowany przez definicję klasy typu o nazwie MyNamespace.MyType,

while .method static void Print(class MyNamespace.MyType test) cil managedprzyjmuje typ obiektu zdefiniowany przez definicję klasy typu obiektu o nazwie MyNamespace.MyType.

podobnie gdyby boxedbyło słowem kluczowym, .method static void Print(boxed MyNamespace.MyType test) cil managedprzyjąłby typ pudełkowy typu wartości zdefiniowanego przez definicję klasy o nazwie MyNamespace.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, objectlub boxed MyNamespace.MyValueTypejako 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.ValueTypeani z żadnego innego typu, ani nie mogą implementować interfejsów. Odpowiednie typy pudełkowe , które również są zdefiniowane , dziedziczą System.ValueTypei mogą implementować interfejsy.

.classDefinicja określa różne rzeczy w zależności od okoliczności.

  • Jeśli określono interfaceatrybut semantyczny, definicja klasy definiuje interfejs.
  • Jeśli interfaceatrybut semantyczny nie jest określony, a definicja się nie rozszerza System.ValueType, definicja klasy definiuje typ obiektu (klasę).
  • Jeśli interfaceatrybut semantyczny nie jest określony, a definicja się rozszerza System.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 w System.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. Gdyby ToString()metoda zdefiniowana w strukturze została wywołana bezpośrednio z typem pudełkowym we thiswskaźniku, przy próbie dostępu do pola Aw MyStruct, 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 jak unboxinstrukcja IL) typ pudełkowy, a następnie statycznie wywołuje ToString()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

I.8.2.4 Dla każdego typu wartości CTS definiuje odpowiadający mu typ odniesienia zwany typem pudełkowym. Odwrotna sytuacja nie jest prawdą: ogólnie typy odwołań nie mają odpowiedniego typu wartości. Reprezentacja wartości typu pudełkowego (wartość w pudełku) to lokalizacja, w której można przechowywać wartość typu wartości. Typ pudełkowy to typ obiektu, a wartość w pudełku to obiekt.

Definiowanie typów wartości

I.8.9.7 Nie wszystkie typy zdefiniowane w definicji klasy są typami obiektów (patrz §I.8.2.3); w szczególności typy wartości nie są typami obiektów, ale są definiowane przy użyciu definicji klasy. Definicja klasy dla typu wartości definiuje zarówno (bez pudełka) typ wartości, jak i powiązany typ pudełkowy (patrz §I.8.2.4). Członkowie definicji klasy definiują reprezentację obu.

II.10.1.3 Atrybuty semantyczne typu określają, czy należy zdefiniować interfejs, klasę czy typ wartości. Atrybut interfejsu określa interfejs. Jeśli ten atrybut nie jest obecny, a definicja rozszerza (bezpośrednio lub pośrednio) System.ValueType, a definicja nie dotyczy System.Enum, należy zdefiniować typ wartości (§II.13). W przeciwnym razie określa się klasę (§II.11).

Typy wartości nie dziedziczą

I.8.9.10 W formie rozpakowanej typy wartości nie dziedziczą po żadnym typie. Typy wartości w pudełkach dziedziczą bezpośrednio po System.ValueType, chyba że są wyliczeniami, w takim przypadku dziedziczą po System.Enum. Typy wartości w pudełkach są zapieczętowane.

II.13 Typy wartości bez pudełka nie są uważane za podtypy innego typu i nie jest właściwe stosowanie instrukcji isinst (patrz część III) w przypadku typów wartości bez pudełka. Instrukcja isinst może być jednak używana dla typów wartości w pudełkach.

I.8.9.10 Typ wartości nie dziedziczy; raczej typ podstawowy określony w definicji klasy definiuje typ podstawowy typu pudełkowego.

Typy wartości nie implementują interfejsów

I.8.9.7 Typy wartości nie obsługują kontraktów interfejsowych, ale powiązane z nimi typy pudełkowe tak.

II.13 Typy wartości muszą implementować zero lub więcej interfejsów, ale ma to znaczenie tylko w formie pudełkowej (§II.13.3).

I.8.2.4 Interfejsy i dziedziczenie definiuje się tylko dla typów referencyjnych. Tak więc, chociaż definicja typu wartości (§I.8.9.7) może określać zarówno interfejsy, które mają być implementowane przez typ wartości, jak i klasę (System.ValueType lub System.Enum), z której dziedziczy, mają one zastosowanie tylko do wartości opakowanych. .

Nieistniejące słowo kluczowe w pudełku

II.13.1 Odniesienie do otwartej formy typu wartości następuje za pomocą słowa kluczowego valueetype, po którym następuje odniesienie do typu. Forma pudełkowa typu wartości powinna być określana za pomocą słowa kluczowego w ramce, po którym następuje odniesienie do typu.

Uwaga: tutaj specyfikacja jest nieprawidłowa, nie ma boxedsł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)myStructw C # tworzy nowe wystąpienie typu pudełkowego typu wartości; nie wykonuje żadnych rzutów. Podobnie, (MyStruct)objw C # unboxing a boxed type, copy the value part out; nie wykonuje żadnych rzutów.

MulleDK19
źródło
1
Wreszcie odpowiedź, która jasno opisuje, jak to działa! Ta zasługuje na akceptowaną odpowiedź. Dobra robota!
Just Shadow