Подтвердить что ты не робот

Невозможно преобразовать тип: зачем нужно бросать дважды?

Учитывая этот упрощенный пример:

abstract class Animal { }

class Dog : Animal
{
  public void Bark() { }
}
class Cat : Animal
{
  public void Mew() { }
}

class SoundRecorder<T> where T : Animal
{
  private readonly T _animal;

  public SoundRecorder(T animal) { _animal = animal; }

  public void RecordSound(string fact)
  {
    if (this._animal is Dog)
    {
      ((Dog)this._animal).Bark(); // Compiler: Cannot convert type 'T' to 'Dog'.
      ((Dog)(Animal)this._animal).Bark(); // Compiles OK
    }
  }
}

Почему компилятор жалуется на листинг типа (Dog)this._animal? Я просто не могу понять, почему компилятор, похоже, нуждается в помощи, выполнив две роли. _animal не может быть чем-то иным, чем Animal, может ли это?

Конечно, этот вопрос мотивирован примером реальной жизни, когда мне пришлось модифицировать существующий код таким образом, что подобный перевод был самым удобным способом сделать это, не рефакторинг всей партии. (Да, используя состав, а не наследование;)).

4b9b3361

Ответ 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();
}

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

Ответ 3

Вероятно, это связано с тем, что вы указываете, что общий тип расширяет Animal, поэтому SoundRecorder можно установить с помощью Cat в качестве общего типа. Поэтому компилятор не может разрешить вам использовать произвольный подкласс Animal для какого-либо другого подкласса Animal. Если вы хотите избежать двойного нажатия, попробуйте сделать следующее:

var dog = _animal as Dog;

if(dog != null)
{
    dog.Bark();
}

В этой статье затрагивается тема создания общих параметров

Ответ 4

Нет явного преобразования типов между Animal и Dog, так как ваши ограничения говорят, что T должен иметь тип Animal. Хотя Dog 'является a Animal, компилятор не знает, что T является Dog. Поэтому он не позволяет вам делать бросок.

Вы можете либо подойти к этому путем неявного преобразования

implicit operator Animal(Dog myClass) 

или может использовать что-то вроде ниже

Dog d = _animal as Dog;