Perché la covarianza e la contraddizione non supportano il tipo di valore


149

IEnumerable<T>è una variante, ma non supporta il tipo di valore, ma solo il tipo di riferimento. Il codice semplice di seguito è stato compilato correttamente:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

Ma cambiando da stringa intsi otterrà un errore compilato:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

Il motivo è spiegato in MSDN :

La varianza si applica solo ai tipi di riferimento; se si specifica un tipo di valore per un parametro di tipo variante, quel parametro di tipo è invariante per il tipo costruito risultante.

Ho cercato e trovato che alcune domande menzionavano il motivo è la boxe tra il tipo di valore e il tipo di riferimento . Ma non mi chiarisce ancora molto perché la boxe è la ragione?

Qualcuno potrebbe fornire una spiegazione semplice e dettagliata perché la covarianza e la contraddizione non supportano il tipo di valore e in che modo la boxe influisce su questo?


3
si veda anche la risposta di Eric alla mia domanda simile: stackoverflow.com/questions/4096299/...
Thorn

Risposte:


126

Fondamentalmente, la varianza si applica quando il CLR può garantire che non è necessario apportare alcuna modifica rappresentativa ai valori. I riferimenti sembrano tutti uguali - quindi puoi usare un IEnumerable<string>as come IEnumerable<object>senza alcun cambiamento nella rappresentazione; il codice nativo stesso non ha bisogno di sapere cosa stai facendo con i valori, purché l'infrastruttura abbia garantito che sarà sicuramente valida.

Per i tipi di valore, che non funziona: per trattare un IEnumerable<int>come IEnumerable<object>, il codice che utilizza la sequenza dovrebbe sapere se eseguire o meno una conversione di boxe.

Potresti leggere il post sul blog di Eric Lippert su rappresentazione e identità per ulteriori informazioni su questo argomento in generale.

EDIT: Dopo aver riletto il post sul blog di Eric, si tratta almeno dell'identità quanto della rappresentazione, sebbene i due siano collegati. In particolare:

Questo è il motivo per cui le conversioni covarianti e controverse di tipi di interfaccia e delegati richiedono che tutti gli argomenti di tipo variabile siano di tipo di riferimento. Per garantire che una conversione di riferimento di variante mantenga sempre l'identità, tutte le conversioni che coinvolgono argomenti di tipo devono anche preservare l'identità. Il modo più semplice per garantire che tutte le conversioni non banali su argomenti di tipo preservino l'identità è di limitarle a conversioni di riferimento.


5
@CuongLe: Beh, in alcuni sensi è un dettaglio di implementazione, ma credo sia la ragione di fondo della restrizione.
Jon Skeet,

2
@ AndréCaron: il post sul blog di Eric è importante qui - non è solo rappresentazione, ma anche conservazione dell'identità. Ma la conservazione della rappresentazione significa che il codice generato non deve preoccuparsene affatto.
Jon Skeet,

1
Precisamente, l'identità non può essere preservata perché intnon è un sottotipo di object. Il fatto che sia richiesto un cambiamento rappresentativo è solo una conseguenza di ciò.
André Caron,

3
In che modo int non è un sottotipo di oggetto? Int32 eredita da System.ValueType, che eredita da System.Object.
David Klempfner,

1
@DavidKlempfner Penso che il commento di @ AndréCaron sia mal definito. Qualsiasi tipo di valore come Int32ha due forme rappresentative, "in box" e "unboxed". Il compilatore deve inserire il codice per convertire da un modulo all'altro, anche se questo è normalmente invisibile a livello di codice sorgente. In effetti, solo il modulo "boxed" è considerato dal sottotipo di un sottotipo object, ma il compilatore lo gestisce automaticamente ogni volta che un tipo di valore viene assegnato a un'interfaccia compatibile o a qualcosa di tipo object.
Steve

10

È forse più facile da capire se si pensa alla rappresentazione sottostante (anche se questo è davvero un dettaglio di implementazione). Ecco una raccolta di stringhe:

IEnumerable<string> strings = new[] { "A", "B", "C" };

Puoi pensare stringsche abbia la seguente rappresentazione:

[0]: riferimento stringa -> "A"
[1]: riferimento stringa -> "B"
[2]: riferimento stringa -> "C"

È una raccolta di tre elementi, ognuno dei quali è un riferimento a una stringa. Puoi lanciarlo in una raccolta di oggetti:

IEnumerable<object> objects = (IEnumerable<object>) strings;

Fondamentalmente è la stessa rappresentazione, tranne ora che i riferimenti sono riferimenti a oggetti:

[0]: riferimento oggetto -> "A"
[1]: riferimento oggetto -> "B"
[2]: riferimento oggetto -> "C"

La rappresentazione è la stessa. I riferimenti sono semplicemente trattati in modo diverso; non puoi più accedere alla string.Lengthproprietà ma puoi comunque chiamare object.GetHashCode(). Confronta questo con una raccolta di ints:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0]: int = 1
[1]: int = 2
[2]: int = 3

Per convertire questo in un IEnumerable<object>i dati devono essere convertiti inscatolando gli ints:

[0]: riferimento oggetto -> 1
[1]: riferimento oggetto -> 2
[2]: riferimento oggetto -> 3

Questa conversione richiede più di un cast.


2
La boxe non è solo un "dettaglio di implementazione". I tipi di valore inscatolati sono memorizzati allo stesso modo degli oggetti di classe e si comportano, per quanto il mondo esterno può dire, come gli oggetti di classe. L'unica differenza è che all'interno della definizione di un tipo di valore inscatolato, si thisriferisce a una struttura i cui campi si sovrappongono a quelli dell'oggetto heap che lo memorizza, anziché fare riferimento all'oggetto che li contiene. Non esiste un modo pulito per un'istanza del tipo di valore inscatolato per ottenere un riferimento all'oggetto heap racchiuso.
supercat

7

Penso che tutto inizi da definiton di LSP(principio di sostituzione di Liskov), che sale:

se q (x) è una proprietà dimostrabile sugli oggetti x di tipo T, allora q (y) dovrebbe essere vero per gli oggetti y di tipo S dove S è un sottotipo di T.

Ma i tipi di valore, ad esempio, intnon possono essere sostituiti da objectin C#. Dimostrare è molto semplice:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

Ciò ritorna falseanche se assegniamo lo stesso "riferimento" all'oggetto.


1
Penso che tu stia usando il principio giusto ma non ci sono prove da fare: intnon è un sottotipo, objectquindi il principio non si applica. La tua "prova" si basa su una rappresentazione intermedia Integer, che è un sottotipo di objecte per il quale la lingua ha una conversione implicita ( object obj1=myInt;è attualmente estesa a object obj1=new Integer(myInt);).
André Caron,

Il linguaggio si occupa del cast corretto tra i tipi, ma il suo comportamento non corrisponde a quello che ci aspetteremmo dal sottotipo di oggetto.
Tigran,

Tutto il mio punto è precisamente che intnon è un sottotipo di object. Inoltre, LSP non si applica perché myInt, obj1e obj2si riferiscono a tre diversi oggetti: uno inte due (nascosto) Integers.
André Caron,

22
@ André: C # non è Java. La intparola chiave del C # è un alias per i BCL System.Int32, che in realtà è un sottotipo di object(un alias di System.Object). In effetti, intla classe base è System.ValueTypechi è la classe base System.Object. Provare a valutare la seguente espressione e vedere: typeof(int).BaseType.BaseType. La ragione per cui ReferenceEqualsqui viene restituito falso è che intè racchiuso in due caselle separate e l'identità di ciascuna casella è diversa per qualsiasi altra casella. Quindi due operazioni di inscatolamento producono sempre due oggetti che non sono mai identici, indipendentemente dal valore inscatolato.
Allon Guralnek,

@AllonGuralnek: ogni tipo di valore (ad esempio System.Int32o List<String>.Enumerator) rappresenta in realtà due tipi di cose: un tipo di posizione di archiviazione e un tipo di oggetto heap (a volte chiamato "tipo di valore in scatola"). Le posizioni di archiviazione di cui derivano i tipi conterranno System.ValueTypela prima; gli oggetti heap i cui tipi fanno allo stesso modo conterranno quest'ultimo. Nella maggior parte delle lingue, esiste un cast allargato dal primo al secondo, e un cast restrittivo dal secondo al primo. Si noti che mentre i tipi di valori inscatolati hanno lo stesso descrittore di tipi delle posizioni di archiviazione dei tipi di valore, ...
supercat

3

Dipende da un dettaglio di implementazione: i tipi di valore sono implementati in modo diverso dai tipi di riferimento.

Se si forzano i tipi di valore a essere trattati come tipi di riferimento (ad esempio, riordinarli, ad es. Facendo riferimento a essi tramite un'interfaccia) è possibile ottenere una varianza.

Il modo più semplice per vedere la differenza è semplicemente considerare un Array: una matrice di tipi di valore è messa insieme in memoria contigua (direttamente), dove come una matrice di tipi di riferimento ha solo il riferimento (un puntatore) contiguo nella memoria; gli oggetti puntati sono assegnati separatamente.

L'altro problema (correlato) (*) è che (quasi) tutti i tipi di riferimento hanno la stessa rappresentazione ai fini della varianza e molto codice non ha bisogno di conoscere la differenza tra i tipi, quindi è possibile (e facilmente) implementato - spesso solo per omissione del controllo extra del tipo).

(*) Potrebbe essere considerato lo stesso problema ...

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.