Почему при проверке ограниченного родового типа происходит сбой прямого действия, но оператор "как"?
`` Я столкнулся с интересным любопытством при компиляции некоторого кода на С#, который использует generics с ограничениями типа. Я нарисовал быстрый пример для иллюстрации. Я использую .NET 4.0 с Visual Studio 2010.
namespace TestCast
{
public class Fruit { }
public class Apple : Fruit { }
public static class Test
{
public static void TestFruit<FruitType>(FruitType fruit)
where FruitType : Fruit
{
if (fruit is Apple)
{
Apple apple = (Apple)fruit;
}
}
}
}
Приведение к Apple не выполняется с ошибкой: "Невозможно преобразовать тип" FruitType "в" TestCast.Apple ". Однако, если я изменяю строку для использования оператора as
, он компилируется без ошибок:
Apple apple = fruit as Apple;
Может кто-нибудь объяснить, почему это так?
Ответы
Ответ 1
Я использовал этот вопрос в качестве основы для статьи в блоге в октябре 2015 года. Спасибо за отличный вопрос!
Может кто-нибудь объяснить, почему это так?
"Почему" вопросы трудно ответить; ответ "из-за того, что говорит спецификация", а затем естественный вопрос: "Почему спецификация говорит это?"
Итак, позвольте мне сделать вопрос более четким:
Какие факторы дизайна языка повлияли на решение о том, чтобы данный оператор-литье был незаконным по параметрам с ограниченным типом?
Рассмотрим следующий сценарий. У вас есть базовый тип Fruit, производные типы Apple и Banana, и теперь это важная часть - пользовательское преобразование от Apple к Banana.
Как вы думаете, что это должно сделать, когда вызывается как M<Apple>
?
void M<T>(T t) where T : Fruit
{
Banana b = (Banana)t;
}
Большинство пользователей, читающих код, скажут, что это должно вызвать пользовательское преобразование из Apple в Banana. Но генераторы С# не являются шаблонами С++; метод не перекомпилирован с нуля для каждой родовой конструкции. Скорее, метод компилируется один раз, и во время этой компиляции значение каждого оператора, включая слепки, определяется для каждого возможного генерического экземпляра.
Тело M<Apple>
должно иметь пользовательское преобразование. Тело M<Banana>
будет иметь преобразование идентичности. M<Cherry>
будет ошибкой. Мы не можем иметь три разных значения оператора в общем методе, поэтому оператор отклоняется.
Вместо этого вам нужно сделать следующее:
void M<T>(T t) where T : Fruit
{
Banana b = (Banana)(object)t;
}
Теперь оба преобразования понятны. Преобразование в объект - это неявное обращение ссылки; преобразование в Banana является явным преобразованием ссылок. Пользовательское преобразование никогда не вызывается, и если оно построено с помощью Cherry, тогда ошибка выполняется во время выполнения, а не время компиляции, как это всегда бывает при кастинге с объекта.
Оператор as
не похож на оператор трансляции; это всегда означает одно и то же, независимо от того, какие типы они заданы, потому что оператор as
никогда не вызывает пользовательское преобразование. Поэтому его можно использовать в контексте, в котором бросок был бы незаконным.
Ответ 2
"Оператор as как операция трансляции. Однако, если преобразование невозможно, так как возвращает значение null вместо создания исключения."
Вы не получаете ошибку времени компиляции с оператором as
, потому что компилятор не проверяет явные приведения undefined при использовании оператора as
; его цель состоит в том, чтобы разрешить попытки выполнения во время выполнения, которые могут быть действительными или нет, а если нет, возвращать null, а не исключать исключение.
В любом случае, если вы планируете обрабатывать случай, когда fruit
не Apple
, вы должны реализовать свою проверку как
var asApple = fruit as Appple;
if(asApple == null)
{
//oh no
}
else
{
//yippie!
}
Ответ 3
Чтобы ответить на вопрос, почему компилятор не позволит вам писать свой код, как вы хотите. Значение if оценивается во время выполнения, поэтому компилятор не знает, что приведение происходит только в том случае, если оно будет действительным.
Чтобы заставить его работать, вы можете "сделать" что-то вроде этого в вашем if:
Apple apple = (Apple)(object)fruit;
Вот еще несколько вопросов по одному и тому же вопросу.
Конечно, наилучшим решением является оператор as
.
Ответ 4
Это объясняется в msdn doc
Оператор as как операция литья. Однако, если преобразование невозможно, так как возвращает null вместо создания исключения. Рассмотрим следующий пример:
выражение типа
Код эквивалентен следующему выражению, за исключением того, что переменная выражения оценивается только один раз.
выражение - это тип? (тип): (тип) null
Обратите внимание, что оператор as выполняет только ссылочные преобразования, преобразования с нулевым значением и преобразования бокса. Оператор as не может выполнять другие преобразования, такие как пользовательские преобразования, которые вместо этого должны выполняться с использованием выражений-выражений.
Ответ 5
Переменная типа базового класса может содержать производный тип. Чтобы получить доступ к методу производного типа, необходимо вернуть значение обратно к производному типу. Использование, так как это предотвратит получение исключения InvalidCastException. Если вы хотите обработать определенный нулевой ссылочный сценарий, вы можете это сделать.
public class Fruit
{
public static explicit operator bool(Fruit D)
{
// handle null references
return D.ToBoolean();
}
protected virtual bool ToBoolean()
{
return false;
}
}