Почему прямое приведение завершается неудачно, но оператор "as" успешно выполняется при тестировании универсального типа с ограничениями?
"Я столкнулся с интересным любопытством при компиляции некоторого кода на C#, в котором используются обобщения с ограничениями типов. Я написал быстрый тестовый пример для иллюстрации. Я использую.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;
Может кто-нибудь объяснить, почему это так?
5 ответов
Я использовал этот вопрос в качестве основы для статьи в блоге в октябре 2015 года. Спасибо за отличный вопрос!
Может кто-нибудь объяснить, почему это так?
На вопросы "почему" сложно ответить; ответ "потому что это то, что говорит спецификация", а затем естественный вопрос: "почему спецификация говорит это?"
Итак, позвольте мне сделать вопрос более четким:
Какие факторы проектирования языка повлияли на решение сделать данный оператор приведения недопустимым для параметров ограниченного типа?
Рассмотрим следующий сценарий. У вас есть базовый тип Fruit, производные типы Apple и Banana, а теперь важная часть - пользовательское преобразование из Apple в Banana.
Как вы думаете, что это должно делать, когда называется M<Apple>
?
void M<T>(T t) where T : Fruit
{
Banana b = (Banana)t;
}
Большинство людей, читающих код, скажут, что это должно вызвать пользовательское преобразование из Apple в Banana. Но дженерики C# не являются шаблонами C++; метод не перекомпилируется с нуля для каждой общей конструкции. Скорее, метод компилируется один раз, и во время этой компиляции значение каждого оператора, включая приведение, определяется для каждого возможного общего экземпляра.
Тело M<Apple>
должен был бы иметь пользовательское преобразование. Тело M<Banana>
будет иметь преобразование личности. M<Cherry>
было бы ошибкой. Мы не можем иметь три разных значения оператора в общем методе, поэтому оператор отклоняется.
Вместо этого вам нужно сделать следующее:
void M<T>(T t) where T : Fruit
{
Banana b = (Banana)(object)t;
}
Теперь оба преобразования понятны. Преобразование в объект является неявным ссылочным преобразованием; преобразование в банан является явным ссылочным преобразованием. Определяемое пользователем преобразование никогда не вызывается, и если оно создается с помощью Cherry, то ошибка возникает во время выполнения, а не во время компиляции, как это всегда происходит при приведении из объекта.
as
оператор не похож на оператор приведения; это всегда означает то же самое, независимо от того, какие типы он дан, потому что as
Оператор никогда не вызывает пользовательское преобразование. Поэтому его можно использовать в контексте, где приведение было бы незаконным.
Вы не получаете ошибку времени компиляции с as
оператор, потому что компилятор не проверяет наличие неопределенных явных приведений при использовании as
оператор; его цель - разрешить попытки приведения во время выполнения, которые могут быть действительными или нет, и, если это не так, вернуть null, а не выдать исключение.
В любом случае, если вы планируете справиться со случаем, когда fruit
не является Apple
, вы должны реализовать свой чек как
var asApple = fruit as Appple;
if(asApple == null)
{
//oh no
}
else
{
//yippie!
}
Чтобы ответить на вопрос, почему компилятор не позволяет вам писать свой код так, как вы хотите. If вычисляется во время выполнения, поэтому компилятор не знает, что приведение произойдет, только если оно будет допустимым.
Чтобы заставить его работать, вы могли бы сделать что-то подобное в вашем if:
Apple apple = (Apple)(object)fruit;
Вот еще немного по тому же вопросу.
Конечно используя as
Оператор - лучшее решение.
Это объясняется в MSDN док
Оператор as похож на операцию приведения. Однако, если преобразование невозможно, as возвращает ноль, а не вызывает исключение. Рассмотрим следующий пример:
выражение как тип Код эквивалентен следующему выражению, за исключением того, что переменная выражения вычисляется только один раз.
выражение есть тип? (тип) выражение: (тип)null Обратите внимание, что оператор as выполняет только ссылочные преобразования, преобразования с нулевым значением и преобразования в боксы. Оператор as не может выполнять другие преобразования, такие как пользовательские преобразования, которые вместо этого должны выполняться с использованием приведенных выражений.
Переменная типа базового класса может содержать производный тип. Чтобы получить доступ к методу производного типа, необходимо привести значение обратно к производному типу. Использование as не позволит вам получить InvalidCastException. Если вы хотите обработать определенный сценарий нулевой ссылки, вы можете сделать это.
public class Fruit
{
public static explicit operator bool(Fruit D)
{
// handle null references
return D.ToBoolean();
}
protected virtual bool ToBoolean()
{
return false;
}
}
В AS
Ключевое слово оператора наследует свою операцию от Visual Basic.
Те, кто в курсе, скажут вам, что Visual Basic - более функциональный язык, чем C#, который сам по себе является постоянной попыткой создать язык синтаксиса, подобный C, но с функциональностью Visual Basic.
Это связано с тем, что языки синтаксиса C более популярны среди профессионалов, как и языки, которые не рекламируют себя как базовые.