Co to jest reifikacja?

163

Wiem, że Java implementuje polimorfizm parametryczny (Generics) z wymazywaniem. Rozumiem, czym jest wymazywanie.

Wiem, że C # implementuje polimorfizm parametryczny z reifikacją. Wiem, że możesz sprawić, że będziesz pisać

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

lub że możesz wiedzieć w czasie wykonywania, jaki jest parametr typu sparametryzowanego typu, ale nie rozumiem, co to jest .

  • Co to jest typ zreifikowany?
  • Co to jest wartość zreifikowana?
  • Co się dzieje, gdy typ / wartość jest reifikowany?
Martijn
źródło
To nie jest odpowiedź, ale może w jakiś sposób pomóc: beust.com/weblog/2011/07/29/erasure-vs-reification
heringer
@heringer, który wydaje się dość dobrze odpowiadać na pytanie „co to jest wymazanie” i wydaje się odpowiadać „co to jest reifikacja” za pomocą „nie wymazać” - częsty temat, który znalazłem, początkowo szukając odpowiedzi przed opublikowaniem tutaj.
Martijn
5
... i pomyślałem, że re ifacja jest procesem przekształcania switchkonstrukcji z powrotem w if/ else, podczas gdy poprzednio została ona przekształcona z an if/ elsena a switch...
Digital Trauma
8
Res , reis to po łacinie rzecz , więc reifikacja jest dosłownie rzeczownikiem . Nie mam nic pożytecznego, jeśli chodzi o użycie tego terminu w języku C #, ale sam fakt, że go użyli, sprawia, że ​​się uśmiecham.
KRyan,

Odpowiedzi:

209

Reifikacja to proces podejmowania abstrakcyjnej rzeczy i tworzenia konkretnej rzeczy.

Termin reifikacja w C # rodzajach ogólnych odnosi się do procesu, w którym definicja typu ogólnego i jeden lub więcej argumentów typu ogólnego (rzecz abstrakcyjna) są łączone, aby utworzyć nowy typ ogólny (konkretna rzecz).

Do wyrażenia to inaczej, jest to proces podejmowania definicji List<T>oraz inti produkcji konkretny List<int>typ.

Aby lepiej to zrozumieć, porównaj następujące podejścia:

  • W języku generycznym języka Java definicja typu ogólnego jest przekształcana zasadniczo w jeden konkretny typ ogólny współużytkowany przez wszystkie dozwolone kombinacje argumentów typów. W ten sposób wiele typów (na poziomie kodu źródłowego) jest mapowanych na jeden typ (na poziomie binarnym) - ale w rezultacie informacje o argumentach typu instancji są odrzucane w tej instancji (usuwanie typu) .

    1. Jako efekt uboczny tej techniki implementacji, jedynymi argumentami typu ogólnego, które są natywnie dozwolone, są te typy, które mogą współużytkować kod binarny swojego konkretnego typu; co oznacza te typy, których miejsca przechowywania mają wymienne reprezentacje; co oznacza typy referencyjne. Używanie typów wartości jako argumentów typu ogólnego wymaga umieszczenia ich w opakowaniu (umieszczenie ich w prostym opakowaniu typu referencyjnego).
    2. Żaden kod nie jest powielany w celu implementacji typów generycznych w ten sposób.
    3. Informacje o typie, które mogły być dostępne w czasie wykonywania (przy użyciu odbicia), zostaną utracone. To z kolei oznacza, że ​​specjalizacja typu ogólnego (możliwość użycia wyspecjalizowanego kodu źródłowego dla dowolnej określonej ogólnej kombinacji argumentów) jest bardzo ograniczona.
    4. Ten mechanizm nie wymaga wsparcia ze strony środowiska wykonawczego.
    5. Istnieje kilka obejść pozwalających zachować informacje o typach, których może używać program Java lub język oparty na JVM.
  • W C # typach ogólnych definicja typu ogólnego jest przechowywana w pamięci w czasie wykonywania. Zawsze, gdy wymagany jest nowy konkretny typ, środowisko uruchomieniowe łączy definicję typu ogólnego i argumenty typu i tworzy nowy typ (reifikacja). Więc otrzymujemy nowy typ dla każdej kombinacji argumentów typu w czasie wykonywania .

    1. Ta technika implementacji umożliwia tworzenie wystąpienia dowolnej kombinacji argumentów typu. Używanie typów wartości jako argumentów typu ogólnego nie powoduje pakowania, ponieważ te typy mają własną implementację. ( Oczywiście boks nadal istnieje w C # - ale zdarza się to w innych scenariuszach, nie w tym).
    2. Duplikowanie kodu może być problemem - ale w praktyce tak nie jest, ponieważ wystarczająco inteligentne implementacje (w tym Microsoft .NET i Mono ) mogą współdzielić kod dla niektórych instancji.
    3. Informacje o typie są utrzymywane, co pozwala w pewnym stopniu na specjalizację poprzez badanie argumentów typu przy użyciu odbicia. Jednak stopień specjalizacji jest ograniczony ze względu na fakt, że definicja typu generycznego jest kompilowana przed jakąkolwiek reifikacją (odbywa się to poprzez kompilację definicji z ograniczeniami parametrów typu - zatem kompilator musi być w stanie „zrozumieć” definicję nawet przy braku argumentów określonego typu ).
    4. Ta technika implementacji w dużym stopniu zależy od obsługi środowiska uruchomieniowego i kompilacji JIT (dlatego często słyszysz, że C # typy generyczne mają pewne ograniczenia na platformach takich jak iOS , gdzie dynamiczne generowanie kodu jest ograniczone).
    5. W kontekście C # typów ogólnych reifikacja jest wykonywana za Ciebie przez środowisko wykonawcze. Jeśli jednak chcesz bardziej intuicyjnie zrozumieć różnicę między definicją typu ogólnego a konkretnym typem ogólnym, zawsze możesz przeprowadzić reifikację samodzielnie, używając System.Typeklasy (nawet jeśli konkretna kombinacja argumentów typu ogólnego, którą tworzysz, nie działa t pojawiają się bezpośrednio w kodzie źródłowym).
  • W szablonach C ++ definicja szablonu jest przechowywana w pamięci w czasie kompilacji. Za każdym razem, gdy w kodzie źródłowym wymagane jest nowe wystąpienie typu szablonu, kompilator łączy definicję szablonu i argumenty szablonu i tworzy nowy typ. Otrzymujemy więc unikalny typ dla każdej kombinacji argumentów szablonu w czasie kompilacji .

    1. Ta technika implementacji umożliwia tworzenie wystąpień dowolnej kombinacji argumentów typu.
    2. Wiadomo, że powoduje to duplikowanie kodu binarnego, ale wystarczająco inteligentny łańcuch narzędzi może to wykryć i udostępnić kod dla niektórych instancji.
    3. Sama definicja szablonu nie jest „kompilowana” - kompilowane są tylko jej konkretne instancje . To nakłada mniej ograniczeń na kompilator i pozwala na większy stopień specjalizacji szablonów .
    4. Ponieważ instancje szablonów są wykonywane w czasie kompilacji, tutaj również nie jest wymagana obsługa środowiska wykonawczego.
    5. Ten proces jest ostatnio nazywany monomorfizacją , szczególnie w społeczności Rust. Słowo to jest używane w przeciwieństwie do polimorfizmu parametrycznego , który jest nazwą pojęcia, z którego pochodzą rodzaje generyczne.
Theodoros Chatzigiannakis
źródło
7
Świetne porównanie z szablonami C ++ ... wydaje się, że znajdują się gdzieś pomiędzy typami C # i Java. Masz inny kod i strukturę do obsługi różnych określonych typów ogólnych, takich jak w C #, ale wszystko odbywa się w czasie kompilacji, jak w Javie.
Luaan,
3
Również w C ++ umożliwia to wprowadzenie specjalizacji szablonowej, w której każdy (lub tylko kilka) typów konkretnych może mieć różne implementacje. Oczywiście niemożliwe w Javie, ale ani w C #.
quetzalcoatl
@quetzalcoatl chociaż jednym z powodów używania tego jest zmniejszenie ilości produkowanego kodu z typami wskaźników, a C # robi coś porównywalnego z typami referencyjnymi za kulisami. Mimo to jest to tylko jeden z powodów, dla których warto tego używać i na pewno są chwile, kiedy specjalizacja szablonów byłaby dobra.
Jon Hanna
W przypadku języka Java warto dodać, że podczas usuwania informacji o typie rzutowania są dodawane przez kompilator, co sprawia, że ​​kod bajtowy jest nie do odróżnienia od kodu bajtowego pre-generycznego.
Rusty Core
27

Reifikacja oznacza ogólnie (poza informatyką) „uczynienie czegoś prawdziwym”.

W programowaniu coś jest reifikowane, jeśli jesteśmy w stanie uzyskać dostęp do informacji o tym w samym języku.

W przypadku dwóch całkowicie niezwiązanych z typami ogólnymi przykładów czegoś, co C # robi i nie zostało zreifikowane, weźmy metody i dostęp do pamięci.

Języki OO na ogół mają metody (i wiele, które nie mają funkcji, które są podobne, ale nie są związane z klasą). W związku z tym możesz zdefiniować metodę w takim języku, wywołać ją, być może nadpisać i tak dalej. Nie wszystkie takie języki pozwalają na traktowanie samej metody jako danych do programu. C # (a tak naprawdę .NET zamiast C #) pozwala na użycie MethodInfoobiektów reprezentujących metody, więc w C # metody są reifikowane. Metody w języku C # to „obiekty pierwszej klasy”.

Wszystkie języki praktyczne mają sposoby na dostęp do pamięci komputera. W języku niskiego poziomu, takim jak C, możemy zajmować się bezpośrednio mapowaniem między adresami numerycznymi używanymi przez komputer, więc takie podejście int* ptr = (int*) 0xA000000; *ptr = 42;jest rozsądne (o ile mamy dobry powód, by podejrzewać, że uzyskanie dostępu do adresu pamięci 0xA000000w ten sposób wygrywa '' coś wysadzić). W C # nie jest to rozsądne (możemy to wymusić w .NET, ale przy zarządzaniu pamięcią .NET przenosząc rzeczy, jest mało prawdopodobne, że będzie przydatne). C # nie ma zreifikowanych adresów pamięci.

Tak więc, ponieważ refied oznacza „ urzeczywistniony ”, „reifikowany typ” jest typem, o którym możemy „rozmawiać” w danym języku.

W przypadku leków generycznych oznacza to dwie rzeczy.

Jednym z nich jest to, że List<string>jest to typ taki, jaki jest stringlub intjest. Możemy porównać ten typ, poznać jego nazwę i zapytać o to:

Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Console.WriteLine(typeof(List<string>) == (42).GetType()); // False
Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True
Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True

Konsekwencją tego jest to, że możemy „mówić” o typach parametrów metody ogólnej (lub metody klasy ogólnej) w samej metodzie:

public static void DescribeType<T>(T element)
{
  Console.WriteLine(typeof(T).FullName);
}
public static void Main()
{
  DescribeType(42);               // System.Int32
  DescribeType(42L);              // System.Int64
  DescribeType(DateTime.UtcNow);  // System.DateTime
}

Z reguły robienie tego zbyt często jest „śmierdzące”, ale ma wiele przydatnych przypadków. Na przykład spójrz na:

public static TSource Min<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  if (value == null)
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      do
      {
        if (!e.MoveNext()) return value;
        value = e.Current;
      } while (value == null);
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (x != null && comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  else
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      if (!e.MoveNext()) throw Error.NoElements();
      value = e.Current;
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  return value;
}

Nie powoduje to wielu porównań między typem TSourcei różnymi typami dla różnych zachowań (zazwyczaj znak, że w ogóle nie powinieneś używać typów ogólnych), ale dzieli się między ścieżką kodu dla typów, które mogą być null(powinny zostać zwrócone, nulljeśli nie znaleziono elementu i nie wolno dokonywać porównań w celu znalezienia minimum, jeśli jeden z porównywanych elementów to null) oraz ścieżkę kodu dla typów, których nie można null(powinna zostać wyrzucona, jeśli nie znaleziono elementu, i nie trzeba się martwić o możliwość nullelementów ).

Ponieważ TSourcejest to „rzeczywiste” w metodzie, to porównanie można wykonać w czasie wykonywania lub w czasie jittingu (ogólnie w czasie jittingu, z pewnością powyższy przypadek robiłby to w czasie jittingu i nie generowałby kodu maszynowego dla ścieżki, która nie została wybrana) i mamy oddzielna „rzeczywista” wersja metody dla każdego przypadku. (Chociaż w ramach optymalizacji kod maszynowy jest współdzielony dla różnych metod dla różnych parametrów typu referencyjnego, ponieważ może to nie wpływać na to, a zatem możemy zmniejszyć ilość jitted kodu maszynowego).

(Nie jest powszechne mówienie o reifikacji typów ogólnych w C #, chyba że masz do czynienia również z Javą, ponieważ w C # po prostu przyjmujemy tę reifikację za pewnik; wszystkie typy są reifikowane. W Javie typy nieogólne są określane jako reifikowane, ponieważ to to rozróżnienie między nimi a typami rodzajowymi).

Jon Hanna
źródło
Nie sądzisz, że możliwość zrobienia tego, co Minpowyżej jest przydatne? W inny sposób bardzo trudno jest spełnić udokumentowane zachowanie.
Jon Hanna
Uważam, że błąd jest (nie) udokumentowanym zachowaniem i implikacją, że to zachowanie jest użyteczne (na marginesie, zachowanie Enumerable.Min<TSource>jest inne, ponieważ nie zgłasza się dla typów bez odwołań do pustej kolekcji, ale zwraca wartość domyślną (TSource) i jest udokumentowane tylko jako „Zwraca minimalną wartość w sekwencji ogólnej”. Twierdziłbym, że oba powinny wrzucić do pustej kolekcji lub że element „zero” powinien zostać przekazany jako linia bazowa, a komparator / funkcja porównania powinna być zawsze przekazywana)
Martijn
1
Byłoby to o wiele mniej przydatne niż bieżąca Min, która pasuje do typowego zachowania db na typach dopuszczających wartość null bez próbowania niemożliwego na typach niedopuszczających wartości null. (Idea linii bazowej nie jest niemożliwa, ale niezbyt przydatna, chyba że istnieje wartość, o której możesz wiedzieć, że nigdy nie będzie w źródle).
Jon Hanna
1
Thingification byłoby lepszą nazwą. :)
tchrist
@tchrist coś może być nierealne.
Jon Hanna
15

Jak już zauważył duffymo , „reifikacja” nie jest kluczową różnicą.

W Javie generyczne są w zasadzie po to, aby ulepszyć obsługę kompilacji - pozwala na użycie silnie wpisanych, np. Kolekcji w kodzie, i zapewnia bezpieczeństwo typów. Jednak istnieje to tylko w czasie kompilacji - skompilowany kod bajtowy nie ma już pojęcia o rodzajach ogólnych; wszystkie typy ogólne są przekształcane w typy „konkretne” (przy użyciu, objectjeśli typ ogólny jest nieograniczony), dodając w razie potrzeby konwersje i sprawdzanie typów.

W .NET typy ogólne są integralną funkcją środowiska CLR. Kiedy kompilujesz typ ogólny, pozostaje on ogólny w wygenerowanym IL. Nie jest po prostu przekształcany w kod nieogólny, jak w Javie.

Ma to wpływ na praktyczne działanie leków generycznych. Na przykład:

  • Java musi SomeType<?>umożliwiać przekazanie dowolnej konkretnej implementacji danego typu generycznego. C # nie może tego zrobić - każdy określony ( zreifikowany ) typ ogólny jest własnym typem.
  • Nieograniczone typy ogólne w Javie oznaczają, że ich wartość jest przechowywana jako plik object. Może to mieć wpływ na wydajność w przypadku używania typów wartości w takich rodzajach ogólnych. W C #, gdy używasz typu wartości w typie ogólnym, pozostaje on typem wartości.

Aby dać przykład, załóżmy, że masz Listtyp ogólny z jednym argumentem ogólnym. W Javie List<String>i List<Int>ostatecznie będą dokładnie tego samego typu w czasie wykonywania - typy ogólne istnieją naprawdę tylko dla kodu w czasie kompilacji. Wszystkie wywołania np. GetValueZostaną przekształcone odpowiednio na (String)GetValuei (Int)GetValue.

W C # List<string>i List<int>są to dwa różne typy. Nie są wymienne, a ich bezpieczeństwo typu jest również wymuszane w czasie wykonywania. Bez względu na to, co robisz, new List<int>().Add("SomeString")będzie nigdy pracy - podstawowa przechowywanie w List<int>to naprawdę jakiś tablica całkowitą, natomiast w Javie, to koniecznie objecttablicą. W C # nie ma żadnych rzutów, żadnego boksu itp.

Powinno to również wyjaśnić, dlaczego C # nie może zrobić tego samego, co Java SomeType<?>. W Javie wszystkie typy generyczne „wywodzące się z” SomeType<?>są dokładnie tego samego typu. W języku C # wszystkie różne określone SomeType<T>typy są odrębnymi typami. Usuwając kontrole w czasie kompilacji, można przejść SomeType<Int>zamiast SomeType<String>(i tak naprawdę wszystko to SomeType<?>oznacza "zignoruj ​​sprawdzenia w czasie kompilacji dla danego typu ogólnego"). W C # nie jest to możliwe, nawet w przypadku typów pochodnych (to znaczy, że nie można tego zrobić, List<object> list = (List<object>)new List<string>();mimo że stringpochodzi z object).

Obie implementacje mają swoje wady i zalety. Było kilka razy, kiedy chciałbym móc po prostu zezwolić SomeType<?>jako argument w C # - ale po prostu nie ma sensu, w jaki sposób działają C # generics.

Luaan
źródło
2
Cóż, można skorzystać z typów List<>, Dictionary<,>i tak dalej w C #, ale różnica między tym a danej listy betonowej lub słownika zajmuje sporo refleksji na moście. Wariancja na interfejsach pomaga w niektórych przypadkach, w których kiedyś chcieliśmy łatwo wypełnić tę lukę, ale nie we wszystkich.
Jon Hanna
2
@JonHanna Możesz użyć List<>do utworzenia wystąpienia nowego określonego typu ogólnego - ale nadal oznacza to utworzenie konkretnego typu, który chcesz. Ale nie możesz List<>na przykład użyć go jako argumentu. Ale tak, przynajmniej pozwala to wypełnić lukę za pomocą odbicia.
Luaan
NET Framework ma trzy zakodowane na stałe ograniczenia ogólne, które nie są typami lokalizacji magazynu; wszystkie inne ograniczenia muszą być typami miejsc przechowywania. Ponadto jedyne sytuacje, w których typ ogólny Tmoże spełnić ograniczenie typu miejsca przechowywania, Uto sytuacja, w której Ti Usą tego samego typu, lub Utyp, który może zawierać odwołanie do wystąpienia T. Nie byłoby możliwe posiadanie znaczącego miejsca przechowywania typu, SomeType<?>ale teoretycznie byłoby możliwe istnienie ogólnego ograniczenia tego typu.
supercat
1
Nie jest prawdą, że w skompilowanym kodzie bajtowym Java nie ma pojęcia generyków. Po prostu instancje klas nie mają pojęcia generyków. To ważna różnica; Pisałem o tym wcześniej na programmers.stackexchange.com/questions/280169/ ... jeśli jesteś zainteresowany.
ruakh
2

Reifikacja jest koncepcją modelowania zorientowanego obiektowo.

Reify to czasownik oznaczający urealnić coś abstrakcyjnego” .

Podczas programowania obiektowego często modeluje się obiekty świata rzeczywistego jako komponenty oprogramowania (np. Okno, przycisk, osoba, bank, pojazd itp.)

Powszechne jest również przekształcanie abstrakcyjnych pojęć w komponenty (np. WindowListener, Broker itp.)

duffymo
źródło
2
Reifikacja to ogólna koncepcja „urzeczywistniania czegoś”, która chociaż ma zastosowanie do modelowania obiektowego, jak mówisz, ma również znaczenie w kontekście implementacji typów ogólnych.
Jon Hanna
2
Więc zostałem wykształcony, czytając te odpowiedzi. Poprawię odpowiedź.
duffymo
2
Ta odpowiedź nie jest odpowiedzią na zainteresowanie PO lekami generycznymi i parametrycznymi polimorfizmami.
Erick G. Hagstrom
Ten komentarz nie odnosi się do niczyich zainteresowań ani nie zwiększa Twojej reputacji. Widzę, że nic nie zaoferowałeś. Moja była pierwszą odpowiedzią i zdefiniowała reifikację jako coś szerszego.
duffymo
1
Twoja odpowiedź mogła być pierwszą, ale odpowiedziałeś na inne pytanie, nie to zadane przez OP, co wynikałoby jasno z treści pytania i jego tagów. Być może nie przeczytałeś dokładnie pytania przed napisaniem odpowiedzi, a może nie wiedziałeś, że termin „reifikacja” ma ustalone znaczenie w kontekście generyków. Tak czy inaczej, twoja odpowiedź nie jest przydatna. Głosuj przeciw.
jcsahnwaldt Przywróć Monikę