Почему при проверке ограниченного родового типа происходит сбой прямого действия, но оператор "как"?

`` Я столкнулся с интересным любопытством при компиляции некоторого кода на С#, который использует 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;
    }
}