La conversión de matriz covariante de xay puede causar una excepción en tiempo de ejecución

142

Tengo una private readonlylista de LinkLabels ( IList<LinkLabel>). Luego agrego LinkLabels a esta lista y agrego esas etiquetas a un me FlowLayoutPanelgusta de la siguiente manera:

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

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

ReSharper me muestra una advertencia: Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation.

Por favor, ayúdame a descubrir:

  1. ¿Qué significa esto?
  2. Este es un control de usuario y no se accederá a varios objetos para configurar etiquetas, por lo que mantener el código como tal no lo afectará.
TheIdiot de la aldea
fuente

Respuestas:

154

Lo que significa es esto

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

Y en términos más generales.

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

En C #, puede hacer referencia a una matriz de objetos (en su caso, LinkLabels) como una matriz de un tipo base (en este caso, como una matriz de Controles). También es tiempo de compilación legal para asignar otro objeto que es un Controla la matriz. El problema es que la matriz no es en realidad una matriz de controles. En tiempo de ejecución, sigue siendo una matriz de LinkLabels. Como tal, la tarea, o escritura, arrojará una excepción.

Anthony Pegram
fuente
Entiendo el tiempo de ejecución / diferencia de tiempo de compilación como en su ejemplo, pero ¿no es legal la conversión de tipo especial a tipo base? Además, he escrito la lista y voy de LinkLabel(tipo especializado) a Control(tipo base).
TheVillageIdiot
2
Sí, la conversión de un LinkLabel a Control es legal, pero no es lo mismo que está sucediendo aquí. Esto es una advertencia sobre la conversión de un LinkLabel[]a Control[], que todavía es legal, pero puede tener un problema de tiempo de ejecución. Todo lo que ha cambiado es la forma en que se hace referencia a la matriz. La matriz en sí no cambia. ¿Ves el problema? La matriz sigue siendo una matriz del tipo derivado. La referencia es a través de una matriz del tipo base. Por lo tanto, es tiempo de compilación legal para asignarle un elemento del tipo base. Sin embargo, el tipo de tiempo de ejecución no lo admitiría.
Anthony Pegram
En su caso, no creo que sea un problema, simplemente está usando la matriz para agregar a una lista de controles.
Anthony Pegram
66
Si alguien se pregunta por qué las matrices son erróneamente covariantes en C #, aquí está la explicación de Eric Lippert : se agregó al CLR porque Java lo requiere y los diseñadores de CLR querían poder admitir lenguajes similares a Java. Luego subimos y lo agregamos a C # porque estaba en el CLR. Esta decisión fue bastante controvertida en ese momento y no estoy muy contento, pero no hay nada que podamos hacer al respecto ahora.
franssu
14

Trataré de aclarar la respuesta de Anthony Pegram.

El tipo genérico es covariante en algún argumento de tipo cuando devuelve valores de dicho tipo (por Func<out TResult>ejemplo TResult, IEnumerable<out T>devuelve instancias de , devuelve instancias de T). Es decir, si algo devuelve instancias de TDerived, también puede trabajar con instancias como si fueran de TBase.

El tipo genérico es contravariante en algún argumento de tipo cuando acepta valores de dicho tipo (por ejemplo, Action<in TArgument>acepta instancias de TArgument). Es decir, si algo necesita instancias de TBase, también puede pasar instancias de TDerived.

Parece bastante lógico que los tipos genéricos que aceptan y devuelven instancias de algún tipo (a menos que se defina dos veces en la firma de tipo genérico, por ejemplo CoolList<TIn, TOut>) no son covariantes ni contravariantes en el argumento de tipo correspondiente. Por ejemplo, Listse define en .NET 4 como List<T>, no List<in T>o List<out T>.

Algunas razones de compatibilidad podrían haber causado que Microsoft ignore ese argumento y haga que las matrices sean covariantes en su argumento de tipo de valores. Tal vez realizaron un análisis y descubrieron que la mayoría de las personas solo usan matrices como si fueran de solo lectura (es decir, solo usan inicializadores de matriz para escribir algunos datos en una matriz) y, como tal, las ventajas superan las desventajas causadas por el posible tiempo de ejecución errores cuando alguien intentará hacer uso de la covarianza al escribir en la matriz. Por lo tanto, está permitido pero no es alentado.

En cuanto a su pregunta original, list.ToArray()crea una nueva LinkLabel[]con valores copiados de la lista original y, para deshacerse de la advertencia (razonable), deberá pasar Control[]a AddRange. list.ToArray<Control>()hará el trabajo: ToArray<TSource>acepta IEnumerable<TSource>como argumento y devuelve TSource[]; List<LinkLabel>implementa solo lectura IEnumerable<out LinkLabel>, que, gracias a la IEnumerablecovarianza, podría pasarse al método que acepta IEnumerable<Control>como argumento.

penartur
fuente
11

La "solución" más directa

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

Ahora, dado que está cambiando covariantemente List<LinkLabel>a, IEnumerable<Control>no hay más preocupaciones, ya que no es posible "agregar" un elemento a un elemento enumerable.

Chris Marisic
fuente
10

La advertencia se debe al hecho de que, en teoría, podría agregar Controlotro que no sea LinkLabela LinkLabel[]través de la Control[]referencia. Esto provocaría una excepción en tiempo de ejecución.

La conversión está sucediendo aquí porque AddRangetoma a Control[].

En términos más generales, la conversión de un contenedor de un tipo derivado a un contenedor de un tipo base solo es seguro si no puede modificar el contenedor posteriormente de la manera que se acaba de describir. Las matrices no satisfacen ese requisito.

Stuart Golodetz
fuente
5

La causa raíz del problema se describe correctamente en otras respuestas, pero para resolver la advertencia, siempre puede escribir:

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

Con VS 2008, no recibo esta advertencia. Esto debe ser nuevo en .NET 4.0.
Aclaración: según Sam Mackrill, es Resharper quien muestra una advertencia.

El compilador de C # no sabe que AddRangeno modificará la matriz que se le pasó. Dado que AddRangetiene un parámetro de tipo Control[], en teoría podría intentar asignar un TextBoxa la matriz, lo que sería perfectamente correcto para una verdadera matriz de Control, pero la matriz es en realidad una matriz de LinkLabelsy no aceptará dicha asignación.

Hacer matrices covariantes en C # fue una mala decisión de Microsoft. Si bien puede parecer una buena idea poder asignar una matriz de un tipo derivado a una matriz de un tipo base en primer lugar, ¡esto puede conducir a errores de tiempo de ejecución!

Olivier Jacot-Descombes
fuente
2
Recibo esta advertencia de Resharper
Sam Mackrill
1

¿Qué tal esto?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Sam Mackrill
fuente
2
Mismo resultado que _list.ToArray<Control>().
jsuddsjr