Когда преобразование типов им не является?

Мне очень часто задают вопрос о логике преобразования типов в языке C#, что, в общем-то, не удивительно. Преобразования типов является распространенной операцией и соответствующие правила довольно запутанные. Вот фрагмент кода, о котором у меня недавно спросили; я упростил его ради ясности:

 class C<T> {}
class D
{
  public static C<U> M<U>(C<bool> c)
  {
    return something;
  }
}
public static class X
{
  public static V Cast<V>(object obj) { return (V)obj; }
}

Где «something» может представлять собой один из трех вариантов:

Вариант 1: (C<U>)c

Вариант 2: X.Cast<C<U>>(c);

Вариант 3: (C<U>)(object)c

Первый вариант не компилируется. Второй и третий варианты успешно компилируются но «падают» во время выполнения, если U не является типом bool.

Вопрос: Почему первый вариант не компилируется?

Поскольку компилятор знает, что данное преобразование может выполняться успешно, только если U является bool, а U может быть чем угодно! Компилятор считает, что в большинстве случаев U не будет типом bool, поэтому данный код наверняка является ошибкой, о чем компилятор и говорит.

Вопрос: Так почему тогда вторая версия компилируется?

Потому что компилятор не знает, что метод X.Cast<V> собирается преобразовывать аргумент к типу V! Все что видит компилятор, так это вызов метода, принимающего объект (и вы передаете объект), работа компилятора на этом заканчивается. С точки зрения компилятора вызов метода является «черным ящиком», и компилятор не заглядывает внутрь метода, чтобы определить, завершится ли неудачно этот метод при заданных входных данных. Это «преобразование» с точки зрения компилятора не является преобразованием, это просто вызов метода.

Вопрос: А как насчет третьего варианта? Почему он компилируется, в отличие от первого варианта?

На самом деле, третий вариант аналогичен второму; все, что мы делаем, это встраиваем вызов метода X.Cast<V>, добавляя при этом преобразование к типу object! Такое преобразование является корректным.

Вопрос: во втором и третьем случае, преобразование компилируется, поскольку в середине происходит конвертация к типу object?

Совершенно верно. Правило следующее: если существует преобразование типа S к объекту, тогда существует явное преобразование объекта к типу S. (*)

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

Вопрос: это объясняет, почему проверка типов времени компиляции нормально не работает с LINQ выражениями?

Да! Можно подумать, что компилятор запретит глупости, типа:

from bool b in new int[] { 123, 345 } select b.ToString();

поскольку не существует преобразования из int к bool, так как же переменная диапазона b может принимать значения массива? Тем не менее, данный код компилируется, поскольку компилятор преобразует этот код к:

(new int[] { 123, 345 }).Cast<bool>().Select(b=>b.ToString())

и компилятор не имеет ни малейшего понятия о том, что последовательность целых чисел передается методу расширения Cast<bool>, который завершится с ошибкой во время выполнения. Этот метод является черным ящиком. Мы с вами знаем, что будет преобразование типов, которое завершится с ошибкой, но компилятор не знает этого.

Кстати, далеко не факт, что мы с вами тоже об этом знаем; возможно, мы используем некоторую библиотеку, отличную от поставщика запросов (query provider) LINQ-to-objects, который знает о возможности преобразования между типами, обычно запрещенными компилятором языка C#. На самом деле, это точка расширения языка, которая прячется за недостатком компилятора: так что это фича, а не баг!

(*) Обратите внимание, что я не сказал: «существует явное преобразование объекта к любому типу», потому что это не так. Можете ли вы назвать тип S, который не преобразовывается к объекту?

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