Конечно, этот вопрос мотивирован примером реальной жизни, когда мне пришлось модифицировать существующий код таким образом, что подобный перевод был самым удобным способом сделать это, не рефакторинг всей партии. (Да, используя состав, а не наследование;)).
Ответ 1
Проблема заключается в том, что компилятор не может гарантировать, что _animal может быть отправлен на Dog, поскольку единственным ограничением, предоставляемым параметром type SoundRecorded, является то, что тип должен быть Animal OR наследуемым от Animal. Итак, компилятор практически думает: что, если вы построите SoundRecorder<Cat>
, операция литья будет недействительной.
Несчастливо (или нет), компилятор недостаточно умен, чтобы убедиться, что вы безопасно защитили свой код от когда-либо достигнутого там, предварительно выполнив проверку "есть".
Если вы должны были сохранить данное животное в качестве реального животного, это не было бы проблемой, поскольку компилятор всегда разрешает любые отливки из базового типа в производный тип. Компилятор не позволяет отбрасывать с Dog на Cat, хотя
РЕДАКТИРОВАТЬ См. ответ Jon Skeets для более конкретного объяснения.
Ответ 2
РЕДАКТИРОВАТЬ: Это попытка повторения ответа Политы - я
думаю, я знаю, что он пытается сказать, но я мог ошибаться.
Мой первоначальный ответ (ниже строки) по-прежнему
канонический: компилятор отвергает его, потому что язык
спецификация говорит, что она должна:) Однако, в попытке угадать
вид дизайнеров языка (я никогда не был частью С#
проектный комитет, и я не думаю, что я спросил их об этом, поэтому
это действительно догадки...) здесь идет...
Мы привыкли думать о действительности конверсий "на
время компиляции "или" во время выполнения ". Обычно неявные преобразования
это те, которые гарантируются во время компиляции:
string x = "foo";
object y = x;
Это не может пойти не так, так что это подразумевается. Если что-то может пойти не так,
язык разработан таким образом, что вы должны сообщить компилятору,
"Поверьте мне, я считаю, что это будет работать во время исполнения, даже если вы
не может гарантировать это сейчас ". Очевидно, что проверка во время выполнения
в любом случае, но вы в основном говорите компилятору, что знаете, что
вы делаете:
object x = "foo";
string y = (string) x;
Теперь компилятор уже предотвращает попытки конверсий
который, по его мнению, никогда не сможет работать 1 полезным способом:
string x = "foo";
Guid y = (Guid) x;
Компилятор не знает, что нет преобразования от string
до Guid
, поэтому
компилятор не верит вашим протестам, что вы знаете, что
вы делаете: вы явно не 2.
Итак, это простые случаи "времени компиляции" и "времени выполнения",
проверка. Но как насчет дженериков? Рассмотрим этот метод:
public Stream ConvertToStream<T>(T value)
{
return (Stream) value;
}
Что компилятор знает? Здесь у нас есть две вещи, которые могут различаться:
значение (которое, конечно, меняется во время выполнения) и тип
параметр T
, который указан в потенциально различном
время компиляции. (Я игнорирую размышления здесь, где даже T
только
известно во время выполнения.) Мы можем скомпилировать код вызова позже,
например:
ConvertToStream<string>(value);
В этот момент метод не имеет смысла, если вы замените тип
параметр T
с string
, вы получите код, который не будет
скомпилировали:
// After type substitution
public Stream ConvertToStream(string value)
{
// Invalid
return (Stream) value;
}
(Generics на самом деле не работает, выполняя подобную подстановку типов
и перекомпилировать, что повлияет на перегрузку и т.д., но оно может
иногда быть полезным способом думать об этом.)
Компилятор не может сообщить, что в тот момент, когда вызов
compiled - вызов не нарушает никаких ограничений на T
, а
тело метода следует рассматривать как деталь реализации. Так
если компилятор хочет запретить вызов метода
таким образом, который вводит нечувствительное преобразование, он должен делать
поэтому, когда сам метод компилируется.
Теперь этот компилятор/язык не всегда согласован в этом подходе.
Например, рассмотрите это изменение на общий метод, и
"после замены типа при вызове с помощью T=string
" версии:
// Valid
public Stream ConvertToStream<T>(T value)
{
return value as Stream;
}
// Invalid
public Stream ConvertToStream(string value)
{
return value as Stream;
}
Этот код компилируется в общей форме, хотя
версия после type замена нет. Так что, может быть, там глубже
причина. Возможно, в некоторых случаях просто не было бы подходящего IL
представляют собой конверсию - и более простые случаи не стоит делать
язык более сложный для...
1 Иногда это становится "неправильным", потому что есть времена
когда преобразование действительно в CLR, но не в С#, например, int[]
до uint[]
. На данный момент я проигнорирую эти крайние случаи.
2 Извиняется тем, кто не любит антропоморфизацию
компилятора в этом ответе. Очевидно, что компилятор не
действительно имеют какой-либо эмоциональный взгляд на разработчика, но я считаю, что это
помогает найти точку.
Простой ответ заключается в том, что компилятор жалуется, потому что спецификация языка говорит, что он должен. Правила приведены в разделе 6.2.7 спецификации С# 4.
Для данного параметра типа T
существуют следующие явные преобразования:
...
- От параметра типа
U
до T
, если T
зависит от U
. (См. Раздел 10.1.5.)
Здесь Dog
не зависит от T
, поэтому не допускается преобразование.
Я подозреваю, что это правило существует, чтобы избежать некоторых неясных краевых дел - в этом случае это немного больно, когда вы можете логически видеть, что это должно быть допустимое попытку преобразования, но я подозреваю, что кодирование этой логики сделает язык более сложный.
Обратите внимание, что альтернативой может быть использование as
вместо is
-then-cast:
Dog dog = this._animal as Dog;
if (dog != null)
{
dog.Bark();
}
Я бы сказал, что этот чист в любом случае с точки зрения выполнения преобразования только один раз.