Warum Kovarianz und Kontravarianz den Wertetyp nicht unterstützen

149

IEnumerable<T>ist eine Co-Variante, unterstützt jedoch keinen Werttyp, sondern nur einen Referenztyp. Der folgende einfache Code wurde erfolgreich kompiliert:

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

Beim Wechsel von stringzu intwird jedoch ein kompilierter Fehler angezeigt:

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

Der Grund wird in MSDN erklärt :

Die Abweichung gilt nur für Referenztypen. Wenn Sie einen Werttyp für einen Variantentypparameter angeben, ist dieser Typparameter für den resultierenden konstruierten Typ unveränderlich.

Ich habe gesucht und festgestellt, dass einige Fragen den Grund für das Boxen zwischen Werttyp und Referenztyp genannt haben . Aber es klärt mich immer noch nicht auf, warum Boxen der Grund ist?

Könnte jemand bitte eine einfache und detaillierte Erklärung geben, warum Kovarianz und Kontravarianz den Werttyp nicht unterstützen und wie sich das Boxen darauf auswirkt?

cuongle
quelle
3
Siehe auch Erics Antwort auf meine ähnliche Frage: stackoverflow.com/questions/4096299/…
thorn̈
1
Mögliches Duplikat von cant-convert-value-type-array-to-params-object
nawfal

Antworten:

126

Grundsätzlich gilt Varianz, wenn die CLR sicherstellen kann, dass keine repräsentativen Änderungen an den Werten vorgenommen werden müssen. Referenzen sehen alle gleich aus - Sie können also eine IEnumerable<string>als IEnumerable<object>ohne Änderung der Darstellung verwenden. Der native Code selbst muss nicht wissen, was Sie mit den Werten tun, solange die Infrastruktur garantiert hat, dass er definitiv gültig ist.

Bei Werttypen funktioniert das nicht - um einen IEnumerable<int>als zu behandelnIEnumerable<object> , müsste der Code, der die Sequenz verwendet, wissen, ob eine Boxkonvertierung durchgeführt werden soll oder nicht.

Vielleicht möchten Sie Eric Lipperts Blog-Beitrag über Repräsentation und Identität lesen um mehr über dieses Thema im Allgemeinen zu erfahren.

EDIT: Nachdem ich Erics Blog-Post selbst noch einmal gelesen habe, geht es mindestens genauso um Identität wie um Repräsentation, obwohl die beiden miteinander verbunden sind. Bestimmtes:

Aus diesem Grund erfordern kovariante und kontravariante Konvertierungen von Schnittstellen- und Delegattypen, dass alle unterschiedlichen Typargumente Referenztypen sind. Um sicherzustellen, dass eine Variantenreferenzkonvertierung immer identitätserhaltend ist, müssen alle Konvertierungen mit Typargumenten auch identitätserhaltend sein. Der einfachste Weg, um sicherzustellen, dass alle nicht trivialen Konvertierungen für Typargumente identitätserhaltend sind, besteht darin, sie auf Referenzkonvertierungen zu beschränken.

Jon Skeet
quelle
5
@ CuongLe: Nun, es ist in gewisser Hinsicht ein Implementierungsdetail, aber ich glaube, es ist der Grund für die Einschränkung.
Jon Skeet
2
@ AndréCaron: Erics Blog-Post ist hier wichtig - es geht nicht nur um Repräsentation, sondern auch um Identitätserhaltung. Die Beibehaltung der Darstellung bedeutet jedoch, dass sich der generierte Code überhaupt nicht darum kümmern muss.
Jon Skeet
1
Genau genommen kann Identität nicht bewahrt werden, da intes sich nicht um einen Subtyp von handelt object. Die Tatsache, dass ein Repräsentationswechsel erforderlich ist, ist nur eine Folge davon.
André Caron
3
Wie ist int kein Subtyp eines Objekts? Int32 erbt von System.ValueType, das von System.Object erbt.
David Klempfner
1
@ DavidKlempfner Ich denke, @ AndréCaron Kommentar ist schlecht formuliert. Jeder Int32Werttyp, z. B. hat zwei Darstellungsformen, "boxed" und "unboxed". Der Compiler muss Code einfügen, um von einem Formular in das andere zu konvertieren, obwohl dies normalerweise auf Quellcodeebene unsichtbar ist. Tatsächlich wird vom zugrunde liegenden System nur das "Boxed" -Formular als Subtyp betrachtet object, aber der Compiler behandelt dies automatisch, wenn ein Werttyp einer kompatiblen Schnittstelle oder einem Typ zugewiesen wird object.
Steve
10

Es ist vielleicht einfacher zu verstehen, wenn Sie über die zugrunde liegende Darstellung nachdenken (obwohl dies wirklich ein Implementierungsdetail ist). Hier ist eine Sammlung von Zeichenfolgen:

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

Sie können sich stringsdie folgende Darstellung vorstellen:

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

Es ist eine Sammlung von drei Elementen, die jeweils auf eine Zeichenfolge verweisen. Sie können dies in eine Sammlung von Objekten umwandeln:

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

Im Grunde ist es die gleiche Darstellung, außer dass die Referenzen jetzt Objektreferenzen sind:

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

Die Darstellung ist die gleiche. Die Referenzen werden nur unterschiedlich behandelt; Sie können nicht mehr auf die string.LengthUnterkunft zugreifen, aber Sie können trotzdem anrufen object.GetHashCode(). Vergleichen Sie dies mit einer Sammlung von Ints:

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

Um dies in ein zu konvertieren, müssen IEnumerable<object>die Daten durch Boxen der Ints konvertiert werden:

[0]: Objektreferenz -> 1
[1]: Objektreferenz -> 2
[2]: Objektreferenz -> 3

Diese Konvertierung erfordert mehr als eine Besetzung.

Martin Liversage
quelle
2
Boxen ist nicht nur ein "Implementierungsdetail". Boxed-Value-Typen werden wie Klassenobjekte gespeichert und verhalten sich, soweit die Außenwelt dies beurteilen kann, wie Klassenobjekte. Der einzige Unterschied besteht darin, dass innerhalb der Definition eines Boxed-Value-Typs thisauf eine Struktur verwiesen wird, deren Felder die des Heap-Objekts überlagern, in dem sie gespeichert sind, anstatt auf das Objekt, das sie enthält. Es gibt keine saubere Möglichkeit für eine Instanz vom Typ Boxed Value, einen Verweis auf das einschließende Heap-Objekt abzurufen.
Supercat
7

Ich denke, alles beginnt mit der Definition von LSP(Liskov-Substitutionsprinzip), die folgende Bedingungen erfüllt:

Wenn q (x) eine Eigenschaft ist, die für Objekte x vom Typ T beweisbar ist, sollte q (y) für Objekte y vom Typ S gelten, wobei S ein Subtyp von T ist.

Werttypen können beispielsweise intnicht durch objectin ersetzt werden C#. Beweisen ist sehr einfach:

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

Dies gibt falseauch dann zurück , wenn wir dem Objekt dieselbe "Referenz" zuweisen .

Tigran
quelle
1
Ich denke, Sie verwenden das richtige Prinzip, aber es gibt keinen Beweis dafür: Es intist kein Subtyp von, objectdaher gilt das Prinzip nicht. Ihr "Beweis" beruht auf einer Zwischendarstellung Integer, die ein Subtyp von ist objectund für die die Sprache eine implizite Konvertierung hat ( object obj1=myInt;wird tatsächlich erweitert object obj1=new Integer(myInt);).
André Caron
Die Sprache sorgt für die korrekte Umwandlung zwischen Typen, aber das Ints-Verhalten entspricht nicht dem, das wir vom Subtyp des Objekts erwarten würden.
Tigran
Mein ganzer Punkt ist genau das intist kein Subtyp von object. Außerdem ist LSP gelten nicht da myInt, obj1und obj2beziehen sich auf drei verschiedene Objekte: ein intund zwei (versteckt) Integers.
André Caron
22
@ André: C # ist nicht Java. Das intSchlüsselwort von C # ist ein Alias ​​für die BCLs System.Int32, der tatsächlich ein Subtyp von object(ein Alias ​​von System.Object) ist. Tatsächlich intist System.ValueTypedie Basisklasse, wer die Basisklasse ist System.Object. Versuchen Sie, den folgenden Ausdruck zu bewerten, und sehen Sie : typeof(int).BaseType.BaseType. Der Grund, warum ReferenceEqualshier false zurückgegeben wird, ist, dass die intBox in zwei separate Boxen unterteilt ist und die Identität jeder Box für jede andere Box unterschiedlich ist. Somit ergeben zwei Boxvorgänge immer zwei Objekte, die unabhängig vom Boxwert niemals identisch sind.
Allon Guralnek
@AllonGuralnek: Jeder Werttyp (z. B. System.Int32oder List<String>.Enumerator) repräsentiert tatsächlich zwei Arten von Dingen: einen Speicherorttyp und einen Heap-Objekttyp (manchmal als "Boxed-Value-Typ" bezeichnet). Speicherorte, von denen die Typen abgeleitet System.ValueTypesind, enthalten die ersteren. Heap-Objekte, deren Typen dies ebenfalls tun, halten letztere. In den meisten Sprachen gibt es eine erweiterte Besetzung von der ersteren und eine sich verengende Besetzung von der letzteren zur ersteren. Beachten Sie, dass Wertetypen in
Boxen denselben Typdeskriptor
3

Es kommt auf ein Implementierungsdetail an: Werttypen werden anders als Referenztypen implementiert.

Wenn Sie erzwingen, dass Werttypen als Referenztypen behandelt werden (z. B. Boxen, z. B. indem Sie über eine Schnittstelle auf sie verweisen), können Sie Abweichungen erhalten.

Der einfachste Weg, um den Unterschied zu erkennen, besteht darin, einfach Folgendes zu betrachten Array: Ein Array von Werttypen wird zusammenhängend (direkt) im Speicher zusammengestellt, wobei ein Array von Referenztypen nur die Referenz (einen Zeiger) zusammenhängend im Speicher hat; Die Objekte, auf die verwiesen wird, werden separat zugeordnet.

Das andere (verwandte) Problem (*) ist, dass (fast) alle Referenztypen für Varianzzwecke dieselbe Darstellung haben und viel Code den Unterschied zwischen den Typen nicht kennen muss, so dass eine Co- und Kontra-Varianz möglich (und leicht) ist implementiert - oft nur durch Weglassen einer zusätzlichen Typprüfung).

(*) Es kann das gleiche Problem gesehen werden ...

Mark Hurd
quelle