#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>
calledEvilList<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 .