Оператор Cast против as вновь - PullRequest
20 голосов
/ 23 мая 2011

Я знаю, что уже есть несколько сообщений о разнице между приведениями и оператором 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. Актерский состав полностью «во время выполнения». Должны ли мы подозревать, что приведение разрешено полностью или частично во время компиляции, а оператор as - нет?

Кстати, добавление ограничения типа неудивительно исправляет приведение, таким образом:

static void MyGenericMethod<T>(T foo) where T : Bar
{
    var myBar1 = foo as Bar;  // compiles
    var myBar2 = (Bar)foo;    // now also compiles
}

Почему оператор as компилируется, а приведение нет?

Ответы [ 5 ]

30 голосов
/ 23 мая 2011

Чтобы ответить на ваш первый вопрос: оператор as не просто игнорирует определенные пользователем преобразования, хотя это имеет значение. Что более важно, так это то, что оператор приведения делает две противоречивые вещи. Оператор приведения означает либо:

  1. Я знаю, что это выражение типа Foo времени компиляции будет на самом деле объектом типа Bar времени выполнения. Компилятор, я говорю вам этот факт сейчас, чтобы вы могли использовать его. Пожалуйста, сгенерируйте код, предполагая, что я прав; если я ошибаюсь, вы можете вызвать исключение во время выполнения.

  2. Я знаю, что это выражение типа Foo времени компиляции будет иметь тип времени выполнения Foo. Существует стандартный способ преобразования некоторых или всех экземпляров Foo в экземпляр Bar. Компилятор, сгенерируйте, пожалуйста, такое преобразование, и если во время выполнения выясняется, что преобразовываемое значение не подлежит преобразованию, генерируйте исключение во время выполнения.

Это противоположности . Отличный трюк - иметь оператора, который делает противоположные вещи.

Оператор as, напротив, имеет только первый смысл. as только упаковывает , распаковывает и сохраняет представление преобразований. Приведение может сделать все это плюс дополнительные преобразования, изменяющие представление. Например, приведение int к short изменяет представление с четырехбайтового целого на двухбайтовое целое.

Вот почему «необработанные» приведения недопустимы для неограниченных генериков; потому что у компилятора недостаточно информации, чтобы выяснить, какой это тип приведения: бокс, распаковка, сохранение представления или изменение представления. Пользователи ожидают, что приведение в обобщенном коде имеет всю семантику приведения в более строго типизированном коде, и у нас нет способа эффективно сгенерировать этот код.

Рассмотрим:

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.

По сути, нам нужно было бы выдать код для теста, который снова запустил компилятор , сделал полный анализ выражений, а затем выпустил новый код. Мы реализовали эту функцию в C # 4; он называется «динамическим», и если вы хотите именно такое поведение, вы можете свободно его использовать.

У нас нет ни одной из этих проблем с as, потому что as делает только три вещи. Он выполняет преобразования в бокс, распаковку и тестирование типов, и мы можем легко сгенерировать код, который выполняет эти три вещи.

5 голосов
/ 23 мая 2011

Должны ли мы подозревать, что актерский состав полностью или частично разрешено при компиляции время а оператор как нет?

Вы дали ответ сами в начале своего вопроса: «Оператор as не будет использовать определяемые пользователем операторы преобразования» - между тем, приведение делает , что означает, что ему нужно найти эти операторы ( или их отсутствие ) во время компиляции.

Обратите внимание, что насколько компилятор касается, нет известных связь между (неизвестно в время компиляции) тип T и Bar.

Тот факт, что тип T является unknown, означает, что компилятор не может узнать, существует ли связь между ним и Bar.

Обратите внимание, что (Bar)(object)foo работает, потому что ни у одного типа не может быть оператора преобразования в Object [так как это базовый класс всего], и, как известно, приведение от объекта к Bar не связано с оператором преобразования. .

2 голосов
/ 23 мая 2011

Это вопрос безопасности типа.
Любой T не может быть преобразован в Bar, но любой T может быть "виден" as a Bar, поскольку поведение четко определено, даже если нет преобразования из T в Bar .

1 голос
/ 23 мая 2011

Первый компилируется просто потому, что так определено ключевое слово as. Если он не может быть разыгран, он вернет null. Это безопасно, потому что само ключевое слово as не вызовет проблем во время выполнения. Тот факт, что вы можете или не можете проверить, что переменная имеет значение null, - это другое дело.

Думайте о as как о методе TryCast.

0 голосов
/ 23 мая 2011

Компилятор не знает, как генерировать код, который будет работать во всех случаях.

Рассмотрим эти два вызова:

MyGenericMethod(new Foo1());
MyGenericMethod(new Foo2());

теперь предположим, что Foo1 содержит оператор приведения, который может преобразовать его в Bar экземпляр, и вместо этого Foo2 убывает с Bar. Очевидно, что используемый код сильно зависит от фактического T, который вы передаете.

В вашем конкретном случае вы говорите, что тип уже является типом Bar, поэтому очевидно, что компилятор может просто выполнить ссылочное преобразование, потому что он знает, что это безопасно, преобразование не происходит или не требуется.

Теперь преобразование as является более "исследовательским", оно не только не учитывает пользовательские преобразования, оно явно учитывает тот факт, что приведение не имеет смысла, поэтому компилятор позволил этому скольжению.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...