Почему я могу отбросить инвариантность IList ?

#c# #c#-4.0 #type-conversion #generic-variance

#c# #c #-4.0 #преобразование типов #общая дисперсия

Вопрос:

В настоящее время я готовлю презентацию новых общих функций дисперсии в C # для своих коллег. Чтобы сократить историю, я написал следующие строки:

 IList<Form> formsList = new List<Form> { new Form(), new Form() };
IList<Control> controlsList = formsList;
  

Да, это, конечно, невозможно, поскольку IList(из T) инвариантен (по крайней мере, моя мысль). Компилятор сообщает мне, что:

Невозможно неявно преобразовать тип System.Collections.Generic.IList<System.Windows.Forms.Form> в System.Collections.Generic.IList<System.Windows.Forms.Control> . Существует явное преобразование (вам не хватает приведения?)

Хм, означает ли это, что я могу принудительно выполнить явное преобразование? Я только что попробовал:

 IList<Form> formsList = new List<Form> { new Form(), new Form() };
IList<Control> controlsList = (IList<Control>)formsList;
  

И … он компилируется! Означает ли это, что я могу отбросить инвариантность? — По крайней мере, компилятор согласен с этим, но я просто превратил прежнюю ошибку времени компиляции в ошибку времени выполнения:

Unable to cast object of type 'System.Collections.Generic.List`1[System.Windows.Forms.Form]' to type 'System.Collections.Generic.IList`1[System.Windows.Forms.Control]'.

Мой вопрос (ы): почему я могу отбросить инвариантность IList<T> (или любого другого инвариантного интерфейса относительно моих экспериментов)? Действительно ли я отбрасываю инвариантность или какое преобразование здесь происходит (как IList(Of Form) и IList(Of Control) совершенно не связаны)? Это темный угол C #, о котором я не знал?

Комментарии:

1. 1 для интереса. Я понятия не имею, как ответить 🙂

2. Привет, ГилЬшалит! Я думаю, Ани понял суть! — Так просто…

Ответ №1:

По сути, тип может реализовываться так IList<Control> же хорошо, как IList<Form> и то, что приведение может быть успешным, поэтому компилятор пропускает его на некоторое время (в стороне: здесь потенциально может быть умнее и выдавать предупреждение, потому что он знает конкретный тип ссылочного объекта, но не знает. Я не думаю, что было бы уместно выдавать ошибку компилятора, поскольку это не является критическим изменением для типа для реализации нового интерфейса).

В качестве примера такого типа:

public class EvilList : IList<Form>, IList<Control> { ... }

То, что происходит во время выполнения, — это просто проверка типа CLR. Исключение, которое вы видите, является типичным для сбоя этой операции.

IL, сгенерированный для приведения, равен:

 castclass [mscorlib]System.Collections.Generic.IList`1<class [System.Windows.Forms]System.Windows.Forms.Control>
  

Из MSDN:

Инструкция castclass пытается привести ссылку на объект (тип O) поверх стека к указанному классу. Новый класс определяется маркером метаданных, указывающим желаемый класс. Если класс объекта в верхней части стека не реализует новый класс (предполагая, что новый класс является интерфейсом) и не является производным классом нового класса, тогда генерируется InvalidCastException . Если ссылка на объект является нулевой ссылкой, castclass завершается успешно и возвращает новый объект как нулевую ссылку.

Исключение InvalidCastException выдается, если объект obj не может быть приведен к классу.

Комментарии:

1. Ага, другой вид преобразования …! Ошибка во время выполнения мне пока понятна. Да, поведение C # мне теперь понятно. Большое спасибо!

2. В более общем плане, по той причине, по которой вы идентифицируете, всегда законно приводить выражение одного типа интерфейса к другому типу интерфейса. Компилятор понятия не имеет, существует или не существует объект, который реализует оба интерфейса. Помните, что приведение означает «Я знаю больше, чем ты, компилятор; Я гарантирую, что это сработает во время выполнения». Если вы не знаете наверняка , что это сработает во время выполнения, тогда не используйте приведение! Вместо этого используйте операторы «is» и «as».

3. Привет, Эрик! Да, я сам очень часто использую метафору «слепок — это контактная линза, которую вы надеваете на компилятор»… Я был слишком слеп, чтобы понять, что здесь происходит… может быть, я заслужил скорее отрицательный голос!

4. @Eric: для случая «неизвестный экземпляр интерфейса -> другой интерфейс» я согласен. Но в этом случае, поскольку компилятор знает, что объект является a List<Form> , не мог бы он хотя бы предупредить? Или, по крайней мере, для более простого случая (IList<Control>)List<Form> . Что-то вроде «На основе доступных метаданных это приведение завершится неудачей». Будет ли это неуместным / очень сложным для работы в компиляторе?

5. Метаданных @Ani недостаточно; для определения фактического типа среды выполнения , чтобы делать то, что вы хотите, потребуется вычислять произвольные выражения. Например, что произойдет, если я получу класс из List<T> called EvilList<T> , который реализует IList<Control> ? Тогда приведение работает, но компилятор не может знать это только на основе типа переменной во время компиляции.

Ответ №2:

Я подозреваю, что в этом случае вы создадите исключение во время выполнения, если попытаетесь добавить новый текстовый блок в свой список элементов управления. Текстовый блок будет соответствовать контракту controlsList, но не formsList .

 IList<Form> formsList = new List<Form> { new Form(), new Form() }; 
IList<Control> controlsList = (IList<Control>)formsList; 
controlsList.Add(New TextBlock); // Should throw at runtime.
  

В этом случае типобезопасная инвариантность обычно выступает в качестве исключения во время выполнения. В этом случае вы можете с уверенностью объявить controlsList как IEnumerable, а не IList (предполагая .Net 4.0), потому что IEnumerable объявлен как ковариантный (IEnumerable). Это решает проблему попытки добавить неправильный тип в список элементов управления, потому что .Add (и другие методы ввода) недоступны из интерфейса out .