Die Konvertierung eines Co-Varianten-Arrays von x nach y kann zu Laufzeitausnahmen führen

142

Ich habe eine private readonlyListe von LinkLabels ( IList<LinkLabel>). Ich füge später LinkLabels zu dieser Liste hinzu und füge diese Bezeichnungen FlowLayoutPanelwie folgt hinzu:

foreach(var s in strings)
{
    _list.Add(new LinkLabel{Text=s});
}

flPanel.Controls.AddRange(_list.ToArray());

Resharper zeigt mir eine Warnung : Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation.

Bitte helfen Sie mir herauszufinden:

  1. Was bedeutet das?
  2. Dies ist ein Benutzersteuerelement, auf das nicht mehrere Objekte zugreifen, um Beschriftungen einzurichten. Wenn Sie also den Code als solchen beibehalten, hat dies keine Auswirkungen darauf.
TheVillageIdiot
quelle

Antworten:

154

Was es bedeutet, ist dies

Control[] controls = new LinkLabel[10]; // compile time legal
controls[0] = new TextBox(); // compile time legal, runtime exception

Und allgemeiner ausgedrückt

string[] array = new string[10];
object[] objs = array; // legal at compile time
objs[0] = new Foo(); // again legal, with runtime exception

In C # dürfen Sie ein Array von Objekten (in Ihrem Fall LinkLabels) als Array eines Basistyps (in diesem Fall als Array von Steuerelementen) referenzieren. Es ist auch zur Kompilierungszeit zulässig , dem Array ein anderes Objekt zuzuweisen , das a ist Control. Das Problem ist, dass das Array eigentlich kein Array von Steuerelementen ist. Zur Laufzeit ist es immer noch ein Array von LinkLabels. Daher wird durch die Zuweisung oder das Schreiben eine Ausnahme ausgelöst.

Anthony Pegram
quelle
Ich verstehe den Unterschied zwischen Laufzeit und Kompilierungszeit wie in Ihrem Beispiel, aber ist die Konvertierung von einem speziellen Typ in einen Basistyp nicht zulässig? Außerdem habe ich eine Liste eingegeben und gehe von LinkLabel(Spezialtyp) zu Control(Basistyp).
TheVillageIdiot
2
Ja, das Konvertieren von einem LinkLabel zu Control ist legal, aber das ist nicht dasselbe wie hier. Dies ist eine Warnung vor dem Konvertieren von a LinkLabel[]nach Control[], was zwar legal ist, aber ein Laufzeitproblem haben kann. Alles, was sich geändert hat, ist die Art und Weise, wie auf das Array verwiesen wird. Das Array selbst wird nicht geändert. Sehen Sie das Problem? Das Array ist immer noch ein Array vom abgeleiteten Typ. Die Referenz erfolgt über ein Array vom Basistyp. Daher ist es zur Kompilierungszeit zulässig, ihm ein Element des Basistyps zuzuweisen. Der Laufzeittyp würde dies jedoch nicht unterstützen.
Anthony Pegram
In Ihrem Fall ist dies meines Erachtens kein Problem. Sie verwenden das Array einfach, um es einer Liste von Steuerelementen hinzuzufügen.
Anthony Pegram
6
Wenn sich jemand fragt, warum Arrays in C # falsch kovariant sind, erklärt Eric Lippert Folgendes : Es wurde der CLR hinzugefügt, weil Java dies erfordert und die CLR-Designer Java-ähnliche Sprachen unterstützen wollten. Wir haben es dann zu C # hinzugefügt, weil es in der CLR war. Diese Entscheidung war damals ziemlich kontrovers und ich bin nicht sehr glücklich darüber, aber wir können jetzt nichts dagegen tun.
Franssu
14

Ich werde versuchen, die Antwort von Anthony Pegram zu klären.

Generischer Typ ist covariant auf irgendeine Art Argument , wenn es Werte des Typs zurückgibt (zB Func<out TResult>kehrt Instanzen TResult, IEnumerable<out T>kehrt Instanzen T). Das heißt, wenn etwas Instanzen von zurückgibt TDerived, können Sie auch mit solchen Instanzen arbeiten, als ob sie von wären TBase.

Der generische Typ ist bei einigen Typargumenten kontravariant, wenn er Werte dieses Typs Action<in TArgument>akzeptiert (z. B. Instanzen von TArgument). Das heißt, wenn etwas Instanzen von benötigt TBase, können Sie auch Instanzen von übergeben TDerived.

Es erscheint ziemlich logisch, dass generische Typen, die Instanzen eines bestimmten Typs akzeptieren und zurückgeben (es sei denn, sie sind beispielsweise zweimal in der generischen CoolList<TIn, TOut>Typensignatur definiert), für das entsprechende Typargument weder kovariant noch kontravariant sind. Beispielsweise Listist in .NET 4 definiert als List<T>, nicht List<in T>oder List<out T>.

Einige Kompatibilitätsgründe haben möglicherweise dazu geführt, dass Microsoft dieses Argument ignoriert und Arrays für ihr Wertetypargument kovariant gemacht hat. Vielleicht haben sie eine Analyse durchgeführt und festgestellt, dass die meisten Leute Arrays nur so verwenden, als wären sie schreibgeschützt (dh sie verwenden nur Array-Initialisierer, um einige Daten in ein Array zu schreiben), und als solche überwiegen die Vorteile die Nachteile, die durch die mögliche Laufzeit verursacht werden Fehler, wenn jemand versucht, beim Schreiben in das Array die Kovarianz zu nutzen. Daher ist es erlaubt, aber nicht ermutigt.

Wie für Ihre ursprüngliche Frage, list.ToArray()eine neue erstellt LinkLabel[]mit den Werten aus ursprünglichen Liste kopiert, und, um loszuwerden, (angemessene) Warnung, müssen Sie in übergeben Control[]zu AddRange. list.ToArray<Control>()wird den Job machen: ToArray<TSource>akzeptiert IEnumerable<TSource>als Argument und kehrt zurück TSource[]; List<LinkLabel>implementiert schreibgeschützt IEnumerable<out LinkLabel>, was dank der IEnumerableKovarianz an die Methode übergeben werden kann, die IEnumerable<Control>als Argument akzeptiert .

penartur
quelle
11

Die einfachste "Lösung"

flPanel.Controls.AddRange(_list.AsEnumerable());

Jetzt, da Sie kovariant zu wechseln List<LinkLabel>, IEnumerable<Control>gibt es keine Bedenken mehr, da es nicht möglich ist, ein Element zu einer Aufzählung hinzuzufügen.

Chris Marisic
quelle
10

Die Warnung ist auf die Tatsache zurückzuführen, dass Sie theoretisch ein Controlanderes als ein LinkLabelzu dem LinkLabel[]durch den Control[]Verweis darauf hinzufügen könnten . Dies würde eine Laufzeitausnahme verursachen.

Die Konvertierung findet hier statt, weil AddRangea Control[].

Im Allgemeinen ist das Konvertieren eines Containers eines abgeleiteten Typs in einen Container eines Basistyps nur dann sicher, wenn Sie den Container anschließend nicht wie oben beschrieben ändern können. Arrays erfüllen diese Anforderung nicht.

Stuart Golodetz
quelle
5

Die Hauptursache des Problems wird in anderen Antworten korrekt beschrieben. Um die Warnung zu beheben, können Sie jedoch immer schreiben:

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));
Tim Williams
quelle
2

Mit VS 2008 erhalte ich diese Warnung nicht. Dies muss neu in .NET 4.0 sein.
Klarstellung: Laut Sam Mackrill ist es Resharper, der eine Warnung anzeigt.

Der C # -Compiler weiß nicht, dass AddRangedas an ihn übergebene Array nicht geändert wird. Da AddRangees einen Parameter vom Typ hat Control[], könnte es theoretisch versuchen, TextBoxdem Array ein zuzuweisen , was für ein echtes Array von vollkommen korrekt wäre Control, aber das Array ist in Wirklichkeit ein Array von LinkLabelsund akzeptiert eine solche Zuordnung nicht.

Es war eine schlechte Entscheidung von Microsoft, Arrays in c # als Co-Variante zu erstellen. Es scheint zwar eine gute Idee zu sein, einem Array eines Basistyps ein Array eines abgeleiteten Typs zuzuweisen, dies kann jedoch zu Laufzeitfehlern führen!

Olivier Jacot-Descombes
quelle
2
Ich bekomme diese Warnung von Resharper
Sam Mackrill
1

Wie wäre es damit?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Sam Mackrill
quelle
2
Gleiches Ergebnis wie _list.ToArray<Control>().
jsuddsjr