"Cast" против "как" повторный оператор
Я знаю, что уже есть несколько сообщений о различии между приведениями и оператором as
. Все они в основном повторяют те же факты:
- Оператор
as
не будет бросать, но возвращает null
, если сбрасывает
- Следовательно, оператор
as
работает только с ссылочными типами
- Оператор
as
не будет использовать пользовательские операторы преобразования
Ответы, как правило, бесконечно обсуждают, как использовать или не использовать ту или иную, а также плюсы и минусы каждого, даже их производительность (которая меня совсем не интересует).
Но здесь есть что-то еще. Рассмотрим:
static void MyGenericMethod<T>(T foo)
{
var myBar1 = foo as Bar; // compiles
var myBar2 = (Bar)foo; // does not compile ('Cannot cast expression of
// type 'T' to type 'Bar')
}
Пожалуйста, не обращайте внимания на то, является ли этот явно несостоятельный пример хорошей практикой или нет. Меня беспокоит очень интересное несоответствие между ними тем, что литье не будет компилироваться, тогда как as
. Я действительно хотел бы знать, может ли кто-нибудь пролить свет на это.
Как часто отмечается, оператор as
игнорирует пользовательские преобразования, но в приведенном выше примере он явно более эффективен для двух. Обратите внимание, что as
в отличие от компилятора не существует известного соединения между типом T и Bar (неизвестным во время компиляции). Бросок полностью "run-time". Должны ли мы подозревать, что литье разрешено, полностью или частично, во время компиляции и оператор as
не?
Кстати, добавление ограничения типа неудивительно исправляет литье, таким образом:
static void MyGenericMethod<T>(T foo) where T : Bar
{
var myBar1 = foo as Bar; // compiles
var myBar2 = (Bar)foo; // now also compiles
}
Почему оператор as
компилируется, а литой нет?
Ответы
Ответ 1
Чтобы решить свой первый вопрос: не только оператор as
игнорирует пользовательские преобразования, хотя это актуально. Что более важно, так это то, что оператор-литье выполняет две противоречивые вещи. Оператор литья означает:
-
Я знаю, что это выражение типа времени компиляции Foo на самом деле будет объектом панели времени исполнения. Компилятор, я рассказываю вам этот факт сейчас, чтобы вы могли его использовать. Пожалуйста, сгенерируйте код, предполагая, что я прав; если я ошибаюсь, вы можете исключить исключение во время выполнения.
-
Я знаю, что это выражение типа времени компиляции Foo будет фактически иметь тип времени исполнения Foo. Существует стандартный способ преобразования некоторых или всех экземпляров Foo в экземпляр Bar. Компилятор, пожалуйста, сгенерируйте такое преобразование, и если во время выполнения окажется, что преобразованное значение не конвертируемо, тогда выведите исключение во время выполнения.
Это противоположности. Аккуратный трюк, чтобы иметь оператора, который делает что-то противоположное.
Оператор as
, напротив, имеет только первый смысл. as
выполняет только боксирование, распаковку и преобразование, сохраняющее представление. Приведение может делать все это плюс дополнительные преобразования, изменяющие представление. Например, при запуске int для краткости изменяется представление из четырехбайтового целого числа в двухбайтовое целое.
Вот почему "сырые" приведения не являются законными в отношении неограниченных дженериков; потому что компилятор не располагает достаточной информацией, чтобы выяснить, что это за кастинг: бокс, распаковка, сохранение или изменение представления. Ожидание пользователей заключается в том, что приведение в родовом коде имеет всю семантику приведения в более строго типизированном коде, и мы не можем эффективно генерировать этот код.
Рассмотрим:
void M<T, U>(T t, out U u)
{
u = (U)t;
}
Ожидаете ли вы этого? Какой код мы создаем, который может обрабатывать:
M<object, string>(...); // explicit reference conversion
M<string, object>(...); // implicit reference conversion
M<int, short>(...); // explicit numeric conversion
M<short, int>(...); // implicit numeric conversion
M<int, object>(...); // boxing conversion
M<object, int>(...); // unboxing conversion
M<decimal?, int?>(...); // lifted conversion calling runtime helper method
// and so on; I could give you literally hundreds of different cases.
В принципе, нам нужно будет испустить код для теста, который снова запустил компилятор, сделал полный анализ выражений и затем выпустил новый код. Мы реализовали эту функцию на С# 4; он называется "динамическим", и если это поведение вы хотите, вы можете свободно его использовать.
У нас нет этих проблем с as
, потому что as
выполняет только три вещи. Он делает преобразования бокса, распаковывает конверсии и тесты типа, и мы можем с легкостью генерировать код, который выполняет эти три вещи.
Ответ 2
Если мы подозреваем, что актерский состав разрешено, полностью или частично, при компиляции времени и оператора as?
Вы ответили сами в начале своего вопроса: "Оператор as не будет использовать пользовательские операторы преобразования" - тем временем, делает бросок, что означает, что ему нужно найти эти операторы (или их отсутствие) при компиляции время.
Обратите внимание, что насколько компилятор обеспокоен, неизвестно связь между время компиляции) типа T и Bar.
Тот факт, что тип T неизвестен, означает, что компилятор не может знать, нет ли связи между ним и Bar.
Обратите внимание, что (Bar)(object)foo
работает, потому что ни один тип не может иметь оператор преобразования для Object [поскольку он является базовым классом всего], и, как известно, отбрасывание из объекта в Bar не должно иметь дело с оператором преобразования.
Ответ 3
Это вопрос безопасности типов.
Любой T
не может преобразовать в Bar
, но любой T
может быть "замечен" as
a Bar
, поскольку поведение корректно определено, даже если нет преобразования от T
до Bar
.
Ответ 4
Первый компилируется просто потому, что определяется ключевое слово as
. Если он не может быть запущен, он вернет null
. Это безопасно, потому что ключевое слово as
само по себе не вызовет каких-либо проблем во время выполнения. Тот факт, что вы можете или не могли проверять, чтобы переменная была нулевой, - это другое дело.
Подумайте о as
как методе TryCast.
Ответ 5
Компилятор не знает, как сгенерировать код, который будет работать для всех случаев.
Рассмотрим эти два вызова:
MyGenericMethod(new Foo1());
MyGenericMethod(new Foo2());
теперь предположим, что Foo1
содержит оператор трансляции, который может преобразовать его в экземпляр Bar
, а Foo2
- от Bar
. Очевидно, что задействованный код будет сильно зависеть от фактического T
, который вы передаете.
В вашем конкретном случае вы говорите, что тип уже является типом Bar
, поэтому, очевидно, компилятор может просто выполнить ссылочное преобразование, потому что он знает, что это безопасно, нет никакого преобразования или необходимости.
Теперь преобразование as
является более "исследовательским", оно не только не учитывает пользовательские преобразования, но и явно указывает на то, что литье не имеет смысла, поэтому компилятор разрешил это слайд.