Konwersja macierzy równoległej z x na y może spowodować wyjątek w czasie wykonywania

142

Mam private readonlylistę LinkLabels ( IList<LinkLabel>). Później dodaję LinkLabels do tej listy i dodaję te etykiety do FlowLayoutPanelnastępujących:

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

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

ReSharper pokazuje mi ostrzeżenie: Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation.

Proszę, pomóż mi zrozumieć:

  1. Co to znaczy?
  2. Jest to kontrolka użytkownika i wiele obiektów nie będzie do niej używać w celu skonfigurowania etykiet, więc zachowanie kodu jako takiego nie ma na nią wpływu.
TheVillageIdiot
źródło

Odpowiedzi:

154

Co to oznacza

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

Mówiąc bardziej ogólnie

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

W C # można odwoływać się do tablicy obiektów (w twoim przypadku LinkLabels) jako tablicy typu podstawowego (w tym przypadku jako tablicy Controls). Dopuszczalne jest również przypisanie do tablicy innego obiektu, który jest a Control. Problem polega na tym, że tablica nie jest w rzeczywistości tablicą kontrolek. W czasie wykonywania nadal jest to tablica LinkLabels. W związku z tym przypisanie lub write zgłosi wyjątek.

Anthony Pegram
źródło
Rozumiem różnicę czasu wykonania / kompilacji, tak jak w przykładzie, ale czy konwersja z typu specjalnego do typu podstawowego jest legalna? Ponadto wpisałem listę i przechodzę od LinkLabel(typ specjalistyczny) do Control(typ podstawowy).
TheVillageIdiot
2
Tak, konwersja z LinkLabel na Control jest legalna, ale to nie to samo, co dzieje się tutaj. Jest to ostrzeżenie przed konwersją z a LinkLabel[]do Control[], która nadal jest legalna, ale może powodować problem w czasie wykonywania. Wszystko, co się zmieniło, to sposób, w jaki odwołuje się do tablicy. Sama tablica nie jest zmieniana. Widzisz problem? Tablica jest nadal tablicą typu pochodnego. Odwołanie odbywa się za pośrednictwem tablicy typu podstawowego. Dlatego dozwolony jest czas kompilacji, aby przypisać do niego element typu podstawowego. Jednak typ środowiska wykonawczego tego nie obsługuje.
Anthony Pegram
W twoim przypadku nie sądzę, że to problem, po prostu używasz tablicy, aby dodać ją do listy kontrolek.
Anthony Pegram,
6
Jeśli ktoś się zastanawia, dlaczego tablice są błędnie kowariantne w C #, oto wyjaśnienie Erica Lipperta : Zostało dodane do CLR, ponieważ wymaga tego Java, a projektanci CLR chcieli mieć możliwość obsługi języków podobnych do Java. Następnie dodaliśmy go do C #, ponieważ był w środowisku CLR. Decyzja ta była wówczas dość kontrowersyjna i nie bardzo mnie to cieszy, ale teraz nic nie możemy na to poradzić.
franssu
14

Spróbuję wyjaśnić odpowiedź Anthony'ego Pegrama.

Typ ogólny jest kowariantny dla argumentu typu, gdy zwraca wartości tego typu (np. Func<out TResult>Zwraca wystąpienia TResult, IEnumerable<out T>zwraca wystąpienia T). Oznacza to, że jeśli coś zwraca wystąpienia programu TDerived, możesz równie dobrze pracować z takimi wystąpieniami, jakby były TBase.

Typ ogólny jest kontrawariantny w przypadku niektórych argumentów typu, gdy akceptuje wartości tego typu (np. Action<in TArgument>Akceptuje wystąpienia TArgument). Oznacza to, że jeśli coś wymaga instancji TBase, możesz równie dobrze przejść w instancjach TDerived.

Wydaje się całkiem logiczne, że typy generyczne, które zarówno akceptują, jak i zwracają instancje pewnego typu (chyba że jest to zdefiniowane dwukrotnie w sygnaturze typu ogólnego, np. CoolList<TIn, TOut>) Nie są kowariantne ani kontrawariantne w odpowiednim argumencie typu. Na przykład Listjest zdefiniowany w .NET 4 jako List<T>, nie List<in T>lub List<out T>.

Niektóre przyczyny zgodności mogły spowodować, że firma Microsoft zignoruje ten argument i sprawi, że tablice będą kowariantne w ich argumencie typu wartości. Być może przeprowadzili analizę i odkryli, że większość ludzi używa tablic tylko tak, jakby były tylko do odczytu (to znaczy używają inicjatorów tablic tylko do zapisania niektórych danych w tablicy) i jako takie, zalety przeważają nad wadami spowodowanymi przez możliwe środowisko uruchomieniowe błędy, gdy ktoś spróbuje skorzystać z kowariancji podczas zapisu do tablicy. Dlatego jest to dozwolone, ale nie zalecane.

Jeśli chodzi o Twoje oryginalne pytanie, list.ToArray()tworzy nowe LinkLabel[]z wartościami skopiowanymi z oryginalnej listy i aby pozbyć się (rozsądnego) ostrzeżenia, musisz przejść Control[]do AddRange. list.ToArray<Control>()wykona zadanie: ToArray<TSource>przyjmuje IEnumerable<TSource>jako argument i zwraca TSource[]; List<LinkLabel>implementuje tylko do odczytu IEnumerable<out LinkLabel>, co dzięki IEnumerablekowariancji mogłoby zostać przekazane do metody akceptującej IEnumerable<Control>jako swój argument.

penartur
źródło
11

Najprostsze „rozwiązanie”

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

Odkąd zmieniasz się kowariantnie List<LinkLabel>na, IEnumerable<Control>nie ma już obaw, ponieważ nie jest możliwe „dodanie” pozycji do wyliczenia.

Chris Marisic
źródło
10

Ostrzeżenie wynika z faktu, że teoretycznie można dodać Controlinny niż a LinkLabeldo LinkLabel[]poprzez Control[]odniesienie do niego. Spowodowałoby to wyjątek w czasie wykonywania.

Konwersja ma miejsce tutaj, ponieważ AddRangetrwa Control[].

Mówiąc bardziej ogólnie, konwersja kontenera typu pochodnego na kontener typu podstawowego jest bezpieczna tylko wtedy, gdy nie można później zmodyfikować kontenera w sposób właśnie przedstawiony. Tablice nie spełniają tego wymagania.

Stuart Golodetz
źródło
5

Główna przyczyna problemu została poprawnie opisana w innych odpowiedziach, ale aby rozwiązać problem, zawsze możesz napisać:

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));
Tim Williams
źródło
2

W VS 2008 nie otrzymuję tego ostrzeżenia. To musi być nowość w .NET 4.0.
Wyjaśnienie: według Sama Mackrilla to Resharper wyświetla ostrzeżenie.

Kompilator C # nie wie, że AddRangenie zmodyfikuje przekazanej do niego tablicy. Ponieważ AddRangema parametr typu Control[], teoretycznie mógłby próbować przypisać a TextBoxdo tablicy, co byłoby całkowicie poprawne dla prawdziwej tablicy Control, ale tablica jest w rzeczywistości tablicą LinkLabelsi nie zaakceptuje takiego przypisania.

Tworzenie kowariancji tablic w języku C # było złą decyzją Microsoftu. Chociaż możliwość przypisania tablicy typu pochodnego do tablicy typu podstawowego może wydawać się dobrym pomysłem, może to prowadzić do błędów w czasie wykonywania!

Olivier Jacot-Descombes
źródło
2
Dostaję to ostrzeżenie od
Resharpera
1

Co powiesz na to?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Sam Mackrill
źródło
2
Taki sam wynik jak _list.ToArray<Control>().
jsuddsjr