Jak wdrażane są leki generyczne?

16

To pytanie z perspektywy wewnętrznych kompilatorów.

Interesują mnie generyczne, a nie szablony (C ++), więc zaznaczyłem pytanie C #. Nie Java, ponieważ AFAIK generyczne w obu językach różnią się implementacjami.

Kiedy patrzę na języki bez generyków, jest to dość proste, możesz zweryfikować definicję klasy, dodać ją do hierarchii i to wszystko.

Ale co zrobić z klasą ogólną, a co ważniejsze, jak obsługiwać odniesienia do niej? Jak upewnić się, że pola statyczne są osobne dla poszczególnych instancji (tj. Za każdym razem, gdy rozstrzygane są parametry ogólne).

Powiedzmy, że widzę połączenie:

var x = new Foo<Bar>();

Czy dodam nową Foo_Barklasę do hierarchii?


Aktualizacja: do tej pory znalazłem tylko 2 odpowiednie posty, ale nawet one nie zawierają zbyt wielu szczegółów w sensie „jak to zrobić samodzielnie”:

Greenoldman
źródło
Upvoting, ponieważ uważam, że pełna odpowiedź byłaby interesująca. Mam kilka pomysłów na temat tego, jak to działa, ale nie dość, aby odpowiedzieć dokładnie. Nie sądzę, że generyczne w C # kompilują się do wyspecjalizowanych klas dla każdego typu ogólnego. Wydaje się, że są one rozwiązywane w czasie wykonywania (może wystąpić zauważalna prędkość uderzenia wynikająca z używania ogólnych). Może uda nam się nakłonić Erica Lipperta do wejścia?
KChaloux
2
@KChaloux: Na poziomie MSIL istnieje jeden opis ogólny. Po uruchomieniu JIT tworzy osobny kod maszynowy dla każdego typu wartości używanego jako parametry ogólne oraz jeszcze jeden zestaw kodu maszynowego obejmujący wszystkie typy referencyjne. Zachowanie ogólnego opisu w MSIL jest naprawdę fajne, ponieważ pozwala tworzyć nowe wystąpienia w czasie wykonywania.
Ben Voigt
@Ben Dlatego nie próbowałem odpowiedzieć na pytanie: p
KChaloux
Nie jestem pewien, czy wciąż jesteś dookoła, ale w jakim języku ty kompilacji do . Będzie to miało duży wpływ na sposób implementacji leków generycznych. Mogę podać informacje o tym, jak zwykle podchodziłem do tego na interfejsie, ale zaplecze może się bardzo różnić.
Telastyn
@Telastyn, na te tematy na pewno jestem :-) Szukam czegoś naprawdę zbliżonego do C #, w moim przypadku kompiluję się do PHP (bez żartów). Będę wdzięczny, jeśli podzielisz się swoją wiedzą.
greenoldman

Odpowiedzi:

4

Jak upewnić się, że pola statyczne są osobne dla poszczególnych instancji (tj. Za każdym razem, gdy rozstrzygane są parametry ogólne).

Każda generalna instancja ma własną kopię (myląco nazwanej) MethodTable, w której przechowywane są pola statyczne.

Powiedzmy, że widzę połączenie:

var x = new Foo<Bar>();

Czy dodam nową Foo_Barklasę do hierarchii?

Nie jestem pewien, czy warto myśleć o hierarchii klas jako o strukturze, która faktycznie istnieje w czasie wykonywania, jest bardziej logiczną konstrukcją.

Ale jeśli weźmiesz pod uwagę MethodTables, każda z pośrednim wskaźnikiem do swojej klasy podstawowej, do utworzenia tej hierarchii, to tak, to dodaje nową klasę do hierarchii.

svick
źródło
Dziękuję, to interesujący kawałek. Więc pola statyczne są rozwiązywane podobnie do wirtualnego stołu, prawda? Czy istnieje odniesienie do słownika „globalnego”, który zawiera wpisy dla każdego typu? Mógłbym więc mieć 2 zespoły nie znające się nawzajem Foo<string>i nie wytwarzałyby z nich dwóch przypadków pola statycznego Foo.
greenoldman
1
@greenoldman Cóż, nie podobny do wirtualnego stołu, dokładnie taki sam. MethodTable przechowuje zarówno pola statyczne, jak i odwołania do metod tego typu, używanych w wirtualnej wysyłce (dlatego nazywa się MethodTable). I tak, CLR musi mieć tabelę, której może użyć, aby uzyskać dostęp do wszystkich tabel metod.
svick,
2

Widzę tam dwa konkretne pytania. Prawdopodobnie chcesz zadać dodatkowe powiązane pytania (jako osobne pytanie z linkiem do tego), aby uzyskać pełne zrozumienie.

W jaki sposób pola statyczne otrzymują osobne wystąpienia dla wystąpienia ogólnego?

Cóż, w przypadku elementów statycznych, które nie są powiązane z parametrami typu ogólnego, jest to dość łatwe (użyj słownika odwzorowanego z parametrów ogólnych na wartość).

Elementy (statyczne lub nie), które są powiązane z parametrami typu, mogą być obsługiwane przez kasowanie typu. Po prostu użyj najsilniejszego ograniczenia (często System.Object). Ponieważ informacje o typie są usuwane po sprawdzeniu typu kompilatora, oznacza to, że sprawdzanie typu środowiska wykonawczego nie będzie potrzebne (chociaż rzutowania interfejsu mogą nadal istnieć w czasie wykonywania).

Czy każda ogólna instancja pojawia się osobno w hierarchii typów?

Nie w ogólnych .NET. Podjęto decyzję o wykluczeniu dziedziczenia z parametrów typu, więc okazuje się, że wszystkie wystąpienia typu ogólnego zajmują to samo miejsce w hierarchii typów.

Prawdopodobnie była to dobra decyzja, ponieważ brak wyszukania nazw z klasy podstawowej byłby niezwykle zaskakujący.

Ben Voigt
źródło
Mój problem polega na tym, że nie mogę oderwać się od myślenia o szablonie. Na przykład - w przeciwieństwie do szablonu, ogólna klasa jest w pełni skompilowana. Oznacza to, że w innym zestawie używającym tej klasy, co się stanie? Już skompilowana metoda jest wywoływana z wewnętrznym rzutowaniem? Wątpię, że leki generyczne mogą polegać na ograniczeniu - raczej na argumencie, inaczej Foo<int>i Foo<string>uderzy te same dane z FooW / O ograniczeń.
greenoldman
1
@greenoldman: Czy możemy na chwilę unikać typów wartości, ponieważ w rzeczywistości są one obsługiwane specjalnie? Jeśli masz List<string>i List<Form>, następnie, ponieważ List<T>ma wewnętrznie członek rodzaju T[]i nie ma żadnych ograniczeń T, to co będziesz rzeczywiście dostać to kod maszynowy, który manipuluje object[]. Ponieważ jednak tylko Tinstancje są umieszczane w tablicy, wszystko, co wychodzi, może zostać zwrócone jako Tbez dodatkowego sprawdzania typu. Z drugiej strony, gdybyś miał ControlCollection<T> where T : Control, wówczas tablica wewnętrzna T[]stałaby się Control[].
Ben Voigt
Czy rozumiem poprawnie, że ograniczenie jest brane i używane jako wewnętrzna nazwa typu, ale kiedy faktycznie używana jest klasa, stosuje się rzutowanie? OK, rozumiem ten model, ale miałem wrażenie, że Java go używa, a nie C #.
greenoldman
3
@greenoldman: Java wykonuje kasowanie typu w kroku tłumaczenia source-> bytecode. Co uniemożliwia weryfikatorowi weryfikację kodu ogólnego. C # robi to w kroku bytecode-> kod maszynowy.
Ben Voigt,
@BenVoigt Niektóre informacje na temat typów ogólnych są przechowywane w Javie, ponieważ w przeciwnym razie nie można byłoby skompilować z klasą używającą typu generycznego bez jej źródła. Po prostu nie jest przechowywany w samej sekwencji kodu bajtowego AIUI, ale raczej w metadanych klasy.
Donal Fellows
1

Ale co zrobić z klasą ogólną, a co ważniejsze, jak obsługiwać odniesienia do niej?

Ogólnym sposobem w interfejsie kompilatora jest posiadanie dwóch rodzajów instancji typu, typ ogólny ( List<T>) i związany typ ogólny ( List<Foo>). Typ ogólny określa, jakie funkcje istnieją, jakie pola i ma odwołania do typów ogólnych, gdziekolwiek Tsą używane. Związany typ ogólny zawiera odwołanie do typu ogólnego i zestaw argumentów typu. Zawiera wystarczającą ilość informacji, aby wygenerować konkretny typ, zastępując ogólne odniesienia Foodo typu argumentami typu lub cokolwiek innego. Tego rodzaju rozróżnienie jest ważne, gdy dokonuje się wnioskowania typu i trzeba wnioskować w List<T>porównaniu z List<Foo>.

Zamiast myśleć o rodzajach ogólnych, takich jak szablony (które bezpośrednio budują różne implementacje), pomocne może być myślenie o nich jak o konstruktorach typu funkcjonalnego języka (gdzie ogólne argumenty są jak argumenty funkcji, która daje ci typ).

Jeśli chodzi o zaplecze, tak naprawdę nie wiem. Cała moja praca z generics była ukierunkowana na CIL jako backend, więc mogłem tam skompilować je do obsługiwanych generics.

Telastyn
źródło
Dziękuję bardzo (szkoda, że ​​nie mogę przyjąć wielu odpowiedzi). Wspaniale jest słyszeć, że właściwie zrobiłem ten krok właściwie - w moim przypadku List<T>zawiera on prawdziwy typ (jego definicję), podczas gdy List<Foo>(dziękuję również za fragment terminologii) moim podejściem zachowuję deklaracje List<T>(oczywiście teraz obowiązujące Foozamiast T).
greenoldman