Как я и говорил в прошлый раз, это загадка была довольно простой: мы получим такое поведение, если FooBar или тип локальной переменной x будут параметром типа (type parameter). Т.е.:

void M<FooBar>()
{
  int x = 0;
  bool b = x is FooBar;  // корректно, true если FooBar - это int.
  FooBar fb = (FooBar)x; // не корректно
}

или

struct FooBar { /* ... */ }
void M<X>()
{
  X x = default(X);
  bool b = x is FooBar; // корректно, true если X - это FooBar
  FooBar fb = (FooBar)x; // некорректно
}

Этот пример показывает не только интересный факт об операторе «is» (который заключается в том, что выражение «is» может давать true, даже когда соответствующее преобразование типов невозможно) но и интересный факт о преобразовании типов. Говоря в общем, преобразование типов разрешено, если во время компиляции известно, что такое преобразование успешно, либо может быть успешным. Но существуют случаи, когда преобразование может быть успешным, но оно запрещено. Почему так происходит? Мне в голову приходит два фактора, основанные на дуальной природе преобразований, о которой я когда-то упоминал: преобразование типов означает «я знаю, что значение точно этого типа, даже если компилятор этого не знает, поэтому компилятор должен его разрешить» и преобразование типов означает «я знаю, что значение точно не этого типа; сгенерируй специальный код для преобразования значения одного типа к значению другого типа».

Но ни один из этих вариантов не подходит при использовании параметров типа. Если рассматривать первое значение преобразования типов, то преобразование параметра типа к конкретному типу означает «я знаю, что параметр типа относится к определенному типу». Но, в таком случае, зачем вообще использовать обобщенные параметры типа? Это все равно, что передавать в функцию в качестве параметра целое число и утверждать, что его значение всегда будет равно 12. Зачем вообще нужно передавать что-то в качестве параметра, если вы заранее знаете, чему будет равен аргумент?

Если же рассматривать второе значение, то в обобщенном коде невозможно сгенерировать специальную логику преобразования. Давайте возьмем наш первый пример. Если FooBar – это double, то придется генерировать другой код для преобразования int к double, по сравнению с вариантом, когда FooBar будет представлять собой long. У нас нет простого и дешевого способа генерации такого кода, а если учесть пользовательские преобразования типов, то нам придется добавить процесс разрешения перегрузки методов во время исполнения. Мы добавили такую возможность в C# 4; если вы хотите генерировать код произвольного преобразования типов во время исполнения, то используйте dynamic.

В следующий раз: мы ответим на вопрос: «при каких условиях оператор “is” выдает предупреждения, что говорит о том, что вызов оператора “is” является избыточным?»

 

Оригинальная статья.