Che cos'è la reificazione?


163

So che Java implementa il polimorfismo parametrico (Generics) con la cancellazione. Capisco cos'è la cancellazione.

So che C # implementa il polimorfismo parametrico con reificazione. So che può farti scrivere

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

o che puoi sapere in fase di esecuzione quale sia il parametro type di un tipo parametrizzato, ma non capisco di cosa si tratti .

  • Che cos'è un tipo reificato?
  • Che cos'è un valore reificato?
  • Cosa succede quando un tipo / valore viene reificato?

Non è una risposta, ma può aiutare in qualche modo: beust.com/weblog/2011/07/29/erasure-vs-reification
heringer

@heringer che sembra rispondere abbastanza bene alla domanda "cos'è la cancellazione", e sembra sostanzialmente rispondere a "cos'è la reificazione" con "non cancellare" - un tema comune che ho trovato quando inizialmente cercavo una risposta prima di pubblicare qui.
Martijn,

5
... e stavo pensando che la rielaborazione ifsia il processo di riconversione di un switchcostrutto in un if/ else, quando era stato precedentemente convertito da un if/ elsein un switch...
Digital Trauma

8
Res , reis è latino per cosa , quindi la reificazione è letteralmente cosa . Non ho nulla di utile da contribuire per quanto riguarda l'uso del termine da parte di C #, ma il fatto in sé e per sé che lo hanno usato mi fa sorridere.
KRyan,

Risposte:


209

La reificazione è il processo di prendere una cosa astratta e creare una cosa concreta.

Il termine reificazione in generici C # si riferisce al processo mediante il quale una definizione di tipo generico e uno o più argomenti di tipo generico (la cosa astratta) vengono combinati per creare un nuovo tipo generico (la cosa concreta).

Per frase in modo diverso, è il processo di prendere la definizione di List<T>e inte produrre un concreto List<int>tipo.

Per capirlo ulteriormente, confronta i seguenti approcci:

  • In generici Java, una definizione di tipo generico viene trasformata essenzialmente in un tipo generico concreto condiviso tra tutte le combinazioni di argomenti di tipo consentite. Pertanto, più tipi (a livello di codice sorgente) vengono associati a un tipo (a livello binario), ma di conseguenza, le informazioni sugli argomenti di tipo di un'istanza vengono scartate in quell'istanza (cancellazione di tipo) .

    1. Come effetto collaterale di questa tecnica di implementazione, gli unici argomenti di tipo generico ammessi nativamente sono quei tipi che possono condividere il codice binario del loro tipo concreto; il che significa quei tipi le cui posizioni di archiviazione hanno rappresentazioni intercambiabili; il che significa tipi di riferimento. L'uso dei tipi di valore come argomenti di tipo generico richiede il loro inscatolamento (collocandoli in un semplice wrapper di tipi di riferimento).
    2. Nessun codice viene duplicato per implementare i generici in questo modo.
    3. Le informazioni sul tipo che avrebbero potuto essere disponibili in fase di esecuzione (usando la riflessione) vanno perse. Questo, a sua volta, significa che la specializzazione di un tipo generico (la capacità di usare un codice sorgente specializzato per una particolare combinazione di argomenti generici) è molto limitata.
    4. Questo meccanismo non richiede supporto dall'ambiente di runtime.
    5. Esistono alcune soluzioni alternative per conservare le informazioni sul tipo che possono essere utilizzate da un programma Java o da un linguaggio basato su JVM.
  • In generici C #, la definizione del tipo generico viene mantenuta in memoria in fase di esecuzione. Ogni volta che è richiesto un nuovo tipo concreto, l'ambiente di runtime combina la definizione del tipo generico e gli argomenti del tipo e crea il nuovo tipo (reificazione). Quindi otteniamo un nuovo tipo per ogni combinazione degli argomenti del tipo, in fase di esecuzione .

    1. Questa tecnica di implementazione consente di istanziare qualsiasi tipo di combinazione di argomenti di tipo. L'uso dei tipi di valore come argomenti di tipo generico non provoca boxe, poiché questi tipi ottengono la propria implementazione. (Il pugilato esiste ancora in C # , ovviamente - ma succede in altri scenari, non in questo.)
    2. La duplicazione del codice potrebbe essere un problema, ma in pratica non lo è, poiché implementazioni sufficientemente intelligenti ( tra cui Microsoft .NET e Mono ) possono condividere il codice per alcune istanze.
    3. Le informazioni sul tipo vengono mantenute, il che consente la specializzazione in una misura, esaminando gli argomenti del tipo usando la riflessione. Tuttavia, il grado di specializzazione è limitato, a causa del fatto che una definizione di tipo generico viene compilata prima che si verifichi qualsiasi reificazione (ciò avviene compilando la definizione rispetto ai vincoli sui parametri di tipo - pertanto, il compilatore deve essere in grado "comprendere" la definizione anche in assenza di argomenti di tipo specifico ).
    4. Questa tecnica di implementazione dipende fortemente dal supporto di runtime e dalla compilazione JIT (motivo per cui spesso si sente che i generici C # hanno alcune limitazioni su piattaforme come iOS , dove la generazione di codice dinamico è limitata).
    5. Nel contesto dei generici C #, la reificazione viene eseguita dall'ambiente di runtime. Tuttavia, se vuoi comprendere in modo più intuitivo la differenza tra una definizione di tipo generico e un tipo generico concreto, puoi sempre eseguire una reificazione da solo, usando la System.Typeclasse (anche se la particolare combinazione di argomenti di tipo generico che stai istanziando non ha fatto t appare direttamente nel tuo codice sorgente).
  • Nei modelli C ++, la definizione del modello viene mantenuta in memoria al momento della compilazione. Ogni volta che è richiesta una nuova istanza di un tipo di modello nel codice sorgente, il compilatore combina la definizione del modello e gli argomenti del modello e crea il nuovo tipo. Quindi otteniamo un tipo unico per ogni combinazione degli argomenti del modello, al momento della compilazione .

    1. Questa tecnica di implementazione consente di istanziare qualsiasi tipo di combinazione di argomenti di tipo.
    2. Questo è noto per duplicare il codice binario, ma una catena di strumenti sufficientemente intelligente potrebbe ancora rilevare questo e condividere il codice per alcune istanze.
    3. La definizione del modello in sé non è "compilata" - solo le sue istanze concrete sono effettivamente compilate . Ciò pone meno vincoli sul compilatore e consente un maggiore grado di specializzazione del modello .
    4. Poiché le istanze del modello vengono eseguite al momento della compilazione, non è necessario nemmeno il supporto di runtime.
    5. Questo processo è recentemente definito monomorfizzazione , specialmente nella comunità Rust. La parola è usata in contrasto con il polimorfismo parametrico , che è il nome del concetto da cui provengono i generici.

7
Ottimo confronto con i modelli C ++ ... sembrano cadere da qualche parte tra i generici di C # e quelli di Java. Hai codice e struttura diversi per la gestione di diversi tipi generici specifici come in C #, ma è tutto fatto in fase di compilazione come in Java.
Luaan,

3
Inoltre, in C ++ ciò consente di introdurre la specializzazione dei modelli, in cui ogni tipo (o solo alcuni) tipi concreti può avere implementazioni diverse. Ovviamente non è possibile in Java, ma nemmeno in C #.
quetzalcoatl,

@quetzalcoatl sebbene uno dei motivi per usarlo sia quello di ridurre la quantità di codice prodotto con i tipi di puntatore, e C # fa qualcosa di paragonabile ai tipi di riferimento dietro le quinte. Tuttavia, questo è solo uno dei motivi per usarlo, e ci sono sicuramente momenti in cui la specializzazione dei modelli sarebbe buona.
Jon Hanna,

Per Java, potresti voler aggiungere che mentre le informazioni sul tipo vengono cancellate, i compilatori vengono aggiunti dal compilatore, rendendo il bytecode indistinguibile dal bytecode pre-generico.
Rusty Core,

27

Reificazione significa generalmente (al di fuori dell'informatica) "rendere qualcosa reale".

Nella programmazione, qualcosa viene reificato se siamo in grado di accedere alle informazioni al riguardo nella lingua stessa.

Per due esempi completamente non generici di qualcosa che C # fa e non ha reificato, prendiamo i metodi e l'accesso alla memoria.

Le lingue OO generalmente hanno metodi (e molti che non hanno funzioni simili ma non legate a una classe). Come tale puoi definire un metodo in una tale lingua, chiamarlo, forse sovrascriverlo e così via. Non tutte queste lingue ti consentono di gestire il metodo stesso come dati di un programma. C # (e in realtà .NET anziché C #) ti consente di utilizzare MethodInfooggetti che rappresentano i metodi, quindi in C # i metodi sono reificati. I metodi in C # sono "oggetti di prima classe".

Tutte le lingue pratiche hanno alcuni mezzi per accedere alla memoria di un computer. In un linguaggio di basso livello come C possiamo occuparci direttamente della mappatura tra gli indirizzi numerici utilizzati dal computer, quindi cose del genere int* ptr = (int*) 0xA000000; *ptr = 42;sono ragionevoli (fintanto che abbiamo una buona ragione per sospettare che l'accesso alla memoria 0xA000000in questo modo abbia vinto ' far esplodere qualcosa). In C # questo non è ragionevole (possiamo semplicemente forzarlo in .NET, ma con la gestione della memoria .NET che sposta le cose in giro non è molto probabile che sia utile). C # non ha indirizzi di memoria reificati.

Quindi, poiché refied significa "reso reale", un "tipo reificato" è un tipo di cui possiamo "parlare" nella lingua in questione.

In generici questo significa due cose.

Uno è che List<string>è un tipo come stringo lo intsono. Possiamo confrontare quel tipo, ottenere il suo nome e chiederci:

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

Una conseguenza di ciò è che possiamo "parlare" dei tipi di parametri di un metodo generico (o metodo di una classe generica) all'interno del metodo stesso:

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
}

Di norma, farlo troppo è "puzzolente", ma ha molti casi utili. Ad esempio, guarda:

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;
}

Questo non fa molti confronti tra il tipo di TSourcee vari tipi per comportamenti diversi (generalmente un segno che non dovresti aver usato affatto generici) ma si divide tra un percorso di codice per tipi che possono essere null(dovrebbe restituire nullse nessun elemento trovato, e non deve fare confronti per trovare il minimo se uno degli elementi a confronto è null) e il percorso del codice per i tipi che non possono essere null(dovrebbero essere lanciati se nessun elemento trovato e non devono preoccuparsi della possibilità di nullelementi ).

Poiché TSourceè "reale" all'interno del metodo, questo confronto può essere effettuato sia in fase di runtime sia in fase di jitting (generalmente tempo di jitting, sicuramente il caso sopra lo farebbe al momento del jitting e non produrrebbe codice macchina per il percorso non preso) e abbiamo un versione "reale" separata del metodo per ciascun caso. (Anche se come ottimizzazione, il codice macchina è condiviso per diversi metodi per diversi parametri del tipo di riferimento, perché può essere senza influire su questo, e quindi possiamo ridurre la quantità di codice macchina escluso).

(Non è comune parlare di reificazione di tipi generici in C # a meno che non si tratti anche di Java, perché in C # diamo per scontata questa reificazione; tutti i tipi sono reificati. In Java, i tipi non generici sono indicati come reificati perché è una distinzione tra loro e tipi generici).


Non pensi di essere in grado di fare ciò Minche è utile sopra? In caso contrario, è molto difficile soddisfare il suo comportamento documentato.
Jon Hanna,

Considero il bug come il comportamento (non) documentato e l'implicazione che tale comportamento è utile (a parte questo, il comportamento di Enumerable.Min<TSource>è diverso in quanto non genera tipi non di riferimento in una raccolta vuota, ma restituisce il valore predefinito (TSource), ed è documentato solo come "Restituisce il valore minimo in una sequenza generica". Direi che entrambi dovrebbero essere lanciati su una raccolta vuota o che un elemento "zero" debba essere passato come linea di base e il comparatore / la funzione di confronto deve sempre essere passata)
Martijn

1
Sarebbe molto meno utile dell'attuale Min, che corrisponde al comportamento db comune su tipi nullable senza tentare l'impossibile su tipi non nullable. (L'idea di base non è impossibile, ma non molto utile a meno che non ci sia un valore che puoi sapere non sarebbe mai nella fonte).
Jon Hanna,

1
Thingification sarebbe stato un nome migliore per questo. :)
tchrist

@tchrist una cosa può essere irreale.
Jon Hanna,

15

Come già notato da Duffymo , la "reificazione" non è la differenza fondamentale.

In Java, i generici sono fondamentalmente lì per migliorare il supporto in fase di compilazione: ti consente di utilizzare ad esempio raccolte fortemente tipizzate nel tuo codice e avere la sicurezza dei tipi gestita per te. Tuttavia, ciò esiste solo al momento della compilazione: il codice byte compilato non ha più alcuna nozione di generici; tutti i tipi generici vengono trasformati in tipi "concreti" (utilizzando objectse il tipo generico non ha limiti), aggiungendo conversioni di tipo e verifiche di tipo secondo necessità.

In .NET, i generici sono parte integrante del CLR. Quando si compila un tipo generico, rimane generico nell'IL generato. Non è solo trasformato in codice non generico come in Java.

Ciò ha diversi effetti sul modo in cui i generici funzionano nella pratica. Per esempio:

  • Java deve SomeType<?>consentire di passare qualsiasi implementazione concreta di un determinato tipo generico. C # non può farlo - ogni tipo generico ( reificato ) specifico è il suo tipo.
  • I tipi generici illimitati in Java indicano che il loro valore è memorizzato come object. Ciò può avere un impatto sulle prestazioni quando si utilizzano tipi di valore in tali generici. In C #, quando si utilizza un tipo di valore in un tipo generico, rimane un tipo di valore.

Per dare un esempio, supponiamo che tu abbia un Listtipo generico con un argomento generico. In Java, List<String>e List<Int>finirà per essere esattamente lo stesso tipo in fase di esecuzione - i tipi generici esistono davvero solo per il codice di compilazione. Tutte le chiamate ad es. GetValueSaranno trasformate in (String)GetValuee (Int)GetValuerispettivamente.

In C #, List<string>e List<int>sono due tipi diversi. Non sono intercambiabili e la loro sicurezza del tipo viene applicata anche in fase di esecuzione. Non importa quello che fai, new List<int>().Add("SomeString")sarà mai lavoro - all'archiviazione sottostante in List<int>è in realtà un po 'di array intero, mentre in Java, è necessariamente un objectarray. In C #, non ci sono cast coinvolti, né boxe ecc.

Ciò dovrebbe anche rendere ovvio il motivo per cui C # non può fare la stessa cosa di Java SomeType<?>. In Java, tutti i tipi generici "derivati ​​da" SomeType<?>finiscono per essere lo stesso tipo esatto. In C #, tutti i vari specifici SomeType<T>sono il loro tipo separato. Rimuovendo i controlli in fase di compilazione, è possibile passare SomeType<Int>anziché SomeType<String>(e in realtà tutto ciò SomeType<?>significa che "ignora i controlli in fase di compilazione per il tipo generico specificato"). In C #, non è possibile, nemmeno per i tipi derivati ​​(ovvero, non è possibile farlo List<object> list = (List<object>)new List<string>();anche se stringè derivato object).

Entrambe le implementazioni hanno i loro pro e contro. Ci sono state alcune volte in cui mi sarebbe piaciuto poter permettere solo SomeType<?>come argomento in C # - ma semplicemente non ha senso il modo in cui funzionano i generici C #.


2
Bene, puoi usare i tipi List<>, Dictionary<,>e così via in C #, ma il divario tra quello e un dato elenco concreto o dizionario richiede un bel po 'di riflessione per colmare. La varianza delle interfacce aiuta in alcuni dei casi in cui una volta avremmo voluto colmare facilmente tale divario, ma non tutti.
Jon Hanna,

2
@JonHanna Puoi usare List<>per creare un'istanza di un nuovo tipo generico specifico, ma significa comunque creare il tipo specifico che desideri. Ma non puoi usare List<>come argomento, per esempio. Ma sì, almeno questo ti consente di colmare il divario usando la riflessione.
Luaan,

.NET Framework ha tre vincoli generici codificati che non sono tipi di ubicazione di archiviazione; tutti gli altri vincoli devono essere tipi di ubicazione di archiviazione. Inoltre, le uniche volte in cui un tipo generico Tpuò soddisfare un vincolo di tipo posizione di archiviazione Usono quando Te Usono dello stesso tipo o Uè un tipo che può contenere un riferimento a un'istanza di T. Non sarebbe possibile avere una posizione di archiviazione significativa del tipo, SomeType<?>ma in teoria sarebbe possibile avere un vincolo generico di quel tipo.
supercat

1
Non è vero che il bytecode Java compilato non abbia nozioni generiche. È solo che le istanze di classe non hanno nozione di generici. Questa è una differenza importante; Ne ho già parlato a programmers.stackexchange.com/questions/280169/… , se sei interessato.
ruakh,

2

La reificazione è un concetto di modellizzazione orientato agli oggetti.

Reify è un verbo che significa "rendere reale qualcosa di astratto" .

Quando si esegue una programmazione orientata agli oggetti, è comune modellare gli oggetti del mondo reale come componenti software (ad esempio Finestra, Pulsante, Persona, Banca, Veicolo, ecc.)

È anche comune reificare concetti astratti anche in componenti (ad esempio WindowListener, Broker, ecc.)


2
La reificazione è un concetto generale di "fare qualcosa di reale" che mentre si applica alla modellazione orientata agli oggetti come dici tu, ha anche un significato nel contesto dell'implementazione dei generici.
Jon Hanna,

2
Quindi sono stato educato leggendo queste risposte. Modificherò la mia risposta.
Duffymo,

2
Questa risposta non ha nulla a che fare con l'interesse dell'OP per i generici e il polimorfismo parametrico.
Erick G. Hagstrom,

Questo commento non fa nulla per soddisfare l'interesse di qualcuno o aumentare il tuo rappresentante. Vedo che non hai offerto nulla. La mia è stata la prima risposta e ha definito la reificazione come qualcosa di più ampio.
Duffymo,

1
La tua risposta potrebbe essere stata la prima, ma hai risposto a una domanda diversa, non quella posta dall'OP, che sarebbe stata chiara dal contenuto della domanda e dai suoi tag. Forse non hai letto attentamente la domanda prima di aver scritto la tua risposta, o forse non sapevi che il termine "reificazione" ha un significato stabilito nel contesto dei generici. In entrambi i casi, la tua risposta non è utile. Downvote.
jcsahnwaldt Reinstate Monica,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.