Conversão de matriz de co-variante de x para y pode causar exceção em tempo de execução

142

Eu tenho uma private readonlylista de LinkLabels ( IList<LinkLabel>). Mais tarde, adiciono LinkLabels a esta lista e adiciono esses rótulos da FlowLayoutPanelseguinte maneira:

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

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

Mostra ReSharper me um aviso: Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation.

Por favor me ajude a descobrir:

  1. O que isso significa?
  2. Este é um controle de usuário e não será acessado por vários objetos para configurar rótulos, portanto, manter o código como tal não o afetará.
TheVillageIdiot
fonte

Respostas:

154

O que isso significa é isso

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

E em termos mais gerais

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

Em C #, você pode fazer referência a uma matriz de objetos (no seu caso, LinkLabels) como uma matriz de um tipo base (nesse caso, como uma matriz de controles). Também é legal em tempo de compilação atribuir outro objeto que é Controla à matriz. O problema é que a matriz não é realmente uma matriz de controles. Em tempo de execução, ainda é uma matriz de LinkLabels. Como tal, a tarefa, ou gravação, lançará uma exceção.

Anthony Pegram
fonte
Entendo a diferença de tempo de execução / compilação, como no seu exemplo, mas a conversão de tipo especial para tipo base é legal? Além disso, digitei list e vou de LinkLabel(tipo especializado) para Control(tipo base).
TheVillageIdiot
2
Sim, a conversão de um LinkLabel para Control é legal, mas não é a mesma coisa que está acontecendo aqui. Este é um aviso sobre a conversão de um LinkLabel[]para Control[], que ainda é legal, mas pode ter um problema de tempo de execução. Tudo o que mudou foi a maneira como a matriz está sendo referenciada. A matriz em si não é alterada. Veja o problema? A matriz ainda é uma matriz do tipo derivado. A referência é através de uma matriz do tipo base. Portanto, é legal em tempo de compilação atribuir um elemento a ele do tipo base. No entanto, o tipo de tempo de execução não o suporta.
Anthony Pegram
No seu caso, não acho que seja um problema, você está simplesmente usando a matriz para adicionar a uma lista de controles.
Anthony Pegram
6
Se alguém está se perguntando por que as matrizes são erroneamente covariantes em C #, aqui está a explicação de Eric Lippert : Ela foi adicionada ao CLR porque o Java exige e os designers do CLR queriam suportar linguagens do tipo Java. Em seguida, o adicionamos ao C # porque estava no CLR. Essa decisão foi bastante controversa na época e não estou muito feliz com isso, mas não há nada que possamos fazer sobre isso agora.
franssu
14

Vou tentar esclarecer a resposta de Anthony Pegram.

O tipo genérico é covariante em algum argumento de tipo quando retorna valores desse tipo (por exemplo, Func<out TResult>retorna instâncias de TResult, IEnumerable<out T>retorna instâncias de T). Ou seja, se algo retornar instâncias de TDerived, você também poderá trabalhar com instâncias como se fossem de TBase.

O tipo genérico é contravariante em algum argumento de tipo quando aceita valores desse tipo (por exemplo, Action<in TArgument>aceita instâncias de TArgument). Ou seja, se algo precisa de instâncias de TBase, você também pode passar em instâncias de TDerived.

Parece bastante lógico que tipos genéricos que aceitam e retornam instâncias de algum tipo (a menos que sejam definidos duas vezes na assinatura de tipo genérica, por exemplo CoolList<TIn, TOut>) não sejam covariantes nem contravariantes no argumento de tipo correspondente. Por exemplo, Listé definido no .NET 4 como List<T>, não List<in T>ou List<out T>.

Alguns motivos de compatibilidade podem ter causado a Microsoft a ignorar esse argumento e tornar as matrizes covariantes em seu argumento de tipo de valores. Talvez eles tenham realizado uma análise e tenham descoberto que a maioria das pessoas usa apenas matrizes como se fossem somente leitura (ou seja, elas usam apenas inicializadores de matriz para gravar alguns dados em uma matriz) e, como tal, as vantagens superam as desvantagens causadas por um possível tempo de execução erros quando alguém tenta usar covariância ao escrever na matriz. Por isso, é permitido, mas não incentivado.

Quanto à sua pergunta original, list.ToArray()cria uma nova LinkLabel[]com os valores copiados da lista original e, para se livrar do aviso (razoável), você precisará passar Control[]para AddRange. list.ToArray<Control>()fará o trabalho: ToArray<TSource>aceita IEnumerable<TSource>como argumento e retorna TSource[]; List<LinkLabel>implementa somente leitura IEnumerable<out LinkLabel>, que, graças à IEnumerablecovariância, pode ser passado para o método que aceita IEnumerable<Control>como argumento.

penartur
fonte
11

A "solução" mais direta

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

Agora, como você está mudando covariantemente List<LinkLabel>para, IEnumerable<Control>não há mais preocupações, pois não é possível "adicionar" um item a um enumerável.

Chris Marisic
fonte
10

O aviso é devido ao fato de que você poderia, teoricamente, adicionar um Controldiferente de LinkLabela LinkLabel[]à Control[]referência a ele. Isso causaria uma exceção de tempo de execução.

A conversão está acontecendo aqui porque AddRangeleva a Control[].

De maneira mais geral, converter um contêiner de um tipo derivado em um contêiner de um tipo base só é seguro se você não puder modificá-lo posteriormente da maneira descrita acima. Matrizes não atendem a esse requisito.

Stuart Golodetz
fonte
5

A causa raiz do problema está descrita corretamente em outras respostas, mas para resolver o aviso, você sempre pode escrever:

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

Com o VS 2008, não estou recebendo esse aviso. Isso deve ser novo no .NET 4.0.
Esclarecimento: de acordo com Sam Mackrill, é o Resharper que exibe um aviso.

O compilador C # não sabe que AddRangenão modificará a matriz passada para ele. Como AddRangetem um parâmetro do tipo Control[], em teoria, poderia tentar atribuir TextBoxa ao array, o que seria perfeitamente correto para um array verdadeiro de Control, mas o array é, na realidade, um array de LinkLabelse não aceitará tal atribuição.

Criar matrizes co-variantes no c # foi uma péssima decisão da Microsoft. Embora possa parecer uma boa ideia poder atribuir uma matriz de um tipo derivado a uma matriz de um tipo base, isso pode levar a erros de tempo de execução!

Olivier Jacot-Descombes
fonte
2
Recebo este aviso do
Resharper
1

Que tal agora?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Sam Mackrill
fonte
2
Mesmo resultado que _list.ToArray<Control>().
Jsuddsjr