La conversion de tableau co-variant de x à y peut provoquer une exception d'exécution

142

J'ai une private readonlyliste de LinkLabels ( IList<LinkLabel>). J'ajoute plus tard des LinkLabels à cette liste et ajoute ces étiquettes à un FlowLayoutPanelcomme suit:

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

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

ReSharper me montre un avertissement: de Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation.

S'il vous plaît, aidez-moi à comprendre:

  1. Que cela veut-il dire?
  2. Il s'agit d'un contrôle utilisateur et ne sera pas accessible par plusieurs objets pour configurer les étiquettes, donc conserver le code en tant que tel ne l'affectera pas.
TheVillageIdiot
la source

Réponses:

154

Ce que cela signifie, c'est ceci

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

Et en termes plus généraux

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

En C #, vous êtes autorisé à référencer un tableau d'objets (dans votre cas, LinkLabels) en tant que tableau d'un type de base (dans ce cas, en tant que tableau de contrôles). Il est également légal au moment de la compilation d'attribuer un autre objet qui est un Controlau tableau. Le problème est que le tableau n'est pas en fait un tableau de contrôles. Au moment de l'exécution, il s'agit toujours d'un tableau de LinkLabels. En tant que tel, l'affectation, ou écriture, lèvera une exception.

Anthony Pegram
la source
Je comprends la différence de temps d'exécution / de compilation comme dans votre exemple, mais la conversion d'un type spécial en type de base n'est-elle pas légale? De plus j'ai tapé la liste et je passe de LinkLabel(type spécialisé) à Control(type de base).
TheVillageIdiot
2
Oui, la conversion d'un LinkLabel en Control est légale, mais ce n'est pas la même chose que ce qui se passe ici. Ceci est un avertissement concernant la conversion de a LinkLabel[]en Control[], qui est toujours légal, mais peut avoir un problème d'exécution. Tout ce qui a changé, c'est la façon dont le tableau est référencé. Le tableau lui-même n'est pas modifié. Vous voyez le problème? Le tableau est toujours un tableau du type dérivé. La référence se fait via un tableau du type de base. Par conséquent, il est légal au moment de la compilation de lui attribuer un élément du type de base. Pourtant, le type d'exécution ne le prendrait pas en charge.
Anthony Pegram
Dans votre cas, je ne pense pas que ce soit un problème, vous utilisez simplement le tableau pour l'ajouter à une liste de contrôles.
Anthony Pegram
6
Si quelqu'un se demande pourquoi les tableaux sont à tort covariants en C #, voici l'explication d'Eric Lippert : il a été ajouté au CLR parce que Java l'exige et que les concepteurs du CLR voulaient être en mesure de prendre en charge les langages de type Java. Nous l'avons ensuite ajouté à C # car il était dans le CLR. Cette décision était assez controversée à l'époque et je n'en suis pas très content, mais nous ne pouvons rien y faire maintenant.
franssu
14

Je vais essayer de clarifier la réponse d'Anthony Pegram.

Le type générique est covariant sur un argument de type lorsqu'il renvoie des valeurs dudit type (par exemple, Func<out TResult>renvoie des instances de TResult, IEnumerable<out T>renvoie des instances de T). Autrement dit, si quelque chose renvoie des instances de TDerived, vous pouvez également travailler avec de telles instances comme si elles étaient de TBase.

Le type générique est contravariant sur certains arguments de type lorsqu'il accepte des valeurs dudit type (par exemple, Action<in TArgument>accepte des instances de TArgument). Autrement dit, si quelque chose nécessite des instances de TBase, vous pouvez également passer des instances de TDerived.

Il semble tout à fait logique que les types génériques qui acceptent et renvoient des instances d'un certain type (à moins qu'il ne soit défini deux fois dans la signature de type générique, par exemple CoolList<TIn, TOut>) ne soient ni covariants ni contravariants sur l'argument de type correspondant. Par exemple, Listest défini dans .NET 4 comme List<T>, pas List<in T>ou List<out T>.

Certaines raisons de compatibilité peuvent avoir amené Microsoft à ignorer cet argument et à rendre les tableaux covariants sur leur argument de type valeurs. Peut-être ont-ils mené une analyse et constaté que la plupart des gens n'utilisent que des tableaux comme s'ils étaient en lecture seule (c'est-à-dire qu'ils n'utilisent que des initialiseurs de tableau pour écrire certaines données dans un tableau), et, en tant que tels, les avantages l'emportent sur les inconvénients causés par une exécution possible. erreurs lorsque quelqu'un essaiera d'utiliser la covariance lors de l'écriture dans le tableau. Par conséquent, il est autorisé mais pas encouragé.

Quant à votre question d'origine, list.ToArray()crée une nouvelle LinkLabel[]avec des valeurs copiées de la liste d'origine, et, pour se débarrasser de l'avertissement (raisonnable), vous devrez passer Control[]à AddRange. list.ToArray<Control>()fera le travail: ToArray<TSource>accepte IEnumerable<TSource>comme argument et retourne TSource[]; List<LinkLabel>implémente en lecture seule IEnumerable<out LinkLabel>, qui, grâce à la IEnumerablecovariance, pourrait être passée à la méthode acceptant IEnumerable<Control>comme argument.

Penartur
la source
11

La «solution» la plus simple

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

Maintenant que vous changez de manière covariante List<LinkLabel>en, IEnumerable<Control>il n'y a plus de soucis puisqu'il n'est pas possible d '«ajouter» un élément à un énumérable.

Chris Marisic
la source
10

L'avertissement est dû au fait que vous pourriez théoriquement ajouter un Controlautre que a LinkLabelau LinkLabel[]par le biais de la Control[]référence. Cela provoquerait une exception d'exécution.

La conversion se produit ici car AddRangeprend un Control[].

Plus généralement, la conversion d'un conteneur d'un type dérivé en un conteneur d'un type de base n'est sûre que si vous ne pouvez pas modifier ultérieurement le conteneur de la manière décrite ci-dessus. Les tableaux ne satisfont pas à cette exigence.

Stuart Golodetz
la source
5

La cause première du problème est correctement décrite dans les autres réponses, mais pour résoudre l'avertissement, vous pouvez toujours écrire:

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

Avec VS 2008, je ne reçois pas cet avertissement. Cela doit être nouveau dans .NET 4.0.
Précision: selon Sam Mackrill c'est Resharper qui affiche un avertissement.

Le compilateur C # ne sait pas que AddRangecela ne modifiera pas le tableau qui lui est passé. Puisqu'il AddRangea un paramètre de type Control[], il pourrait en théorie essayer d'attribuer a TextBoxau tableau, ce qui serait parfaitement correct pour un vrai tableau de Control, mais le tableau est en réalité un tableau de LinkLabelset n'acceptera pas une telle affectation.

La co-variante de tableaux en c # était une mauvaise décision de Microsoft. Bien que cela puisse sembler une bonne idée de pouvoir attribuer un tableau d'un type dérivé à un tableau d'un type de base en premier lieu, cela peut entraîner des erreurs d'exécution!

Olivier Jacot-Descombes
la source
2
Je reçois cet avertissement de Resharper
Sam Mackrill
1

Que dis-tu de ça?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Sam Mackrill
la source
2
Même résultat que _list.ToArray<Control>().
jsuddsjr