Есть ли разница между "x is int?" И "x is int" в С#?

class C<T> where T : struct {
    bool M1(object o) => o is T;
    bool M2(object o) => o is T?;
}

Два вышеприведенных метода ведут себя одинаково, как при передаче null reference, так и в боксе T. Однако сгенерированный код MSIL немного отличается:

.method private hidebysig instance bool M1(object o) cil managed {
    .maxstack 8
    IL_0000: ldarg.1
    IL_0001: isinst !T
    IL_0006: ldnull
    IL_0007: cgt.un
    IL_0009: ret
}

против

.method private hidebysig instance bool M2(object o) cil managed {
    .maxstack 8
    IL_0000: ldarg.1
    IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!T>
    IL_0006: ldnull
    IL_0007: cgt.un
    IL_0009: ret
}

Как вы можете видеть, выражение o is T? фактически выполняет проверку типа для типа Nullable<T>, несмотря на то, что типы с нулевым значением специально обрабатываются с помощью CLR, так что С# представляет значение в коробке T? как null reference (if T? не имеет значения) или значение в коробке T. Кажется невозможным получить поле типа Nullable<T> в чистом С# или, возможно, даже в С++/CLI (поскольку runtime обрабатывает код операции box для поддержки этого "T? = > T box/null" бокса).

Я что-то упустил или o is T? практически эквивалентен o is T в С#?

Ответ 1

В соответствии со спецификацией (выделение), в E is T, типы с нулевыми значениями T и соответствующие типы с нулевым значением обрабатываются одинаково:

7.10.10 Оператор is

Оператор is используется для динамической проверки того, совместим ли тип времени выполнения с данным типом. Результат операции E is T, где E является выражением, а T является типом, является логическим значением, указывающим, может ли E быть успешно преобразован в тип T посредством ссылочного преобразования, преобразования бокса, или преобразование распаковки. Операция оценивается следующим образом, после того, как аргументы типа были заменены всеми параметрами типа:

  • Если E является анонимной функцией, возникает ошибка времени компиляции

  • Если E - это группа методов или нулевой литерал, если тип E является ссылочным типом или типом NULL, а значение E равно null, результат равен false.

  • В противном случае пусть D представляет динамический тип E следующим образом:

    • Если тип E является ссылочным типом, D является типом времени выполнения справки экземпляра E.
    • Если тип E является нулевым типом, D является базовым типом этого типа с нулевым значением.

    • Если тип E - тип значения, не равный nullable, D является типом E.

  • Результат операции зависит от D и T следующим образом:

    • Если T является ссылочным типом, результат имеет значение true, если D и T являются одним и тем же типом, если D является ссылочным типом и неявным эталонным преобразованием от D до T существует, или если D - тип значения, и существует преобразование бокса из D в T.
    • Если T - тип с нулевым значением, результат будет истинным, если D является базовым типом T.
    • Если T - тип значения, не равный nullable, результат будет истинным, если D и T являются одним и тем же типом.
    • В противном случае результат будет ложным.

Ответ 2

Рассмотрим этот общий метод:

static bool Is<T>(object arg)
{
    return arg is T;
}

Важнейшая часть этого метода скомпилируется в isinst !!T. Теперь вы ожидаете, что Is<int?>(arg) будет вести себя точно так же, как arg is int?, не так ли? Чтобы обеспечить эту точную согласованность, компилятор С# должен испускать один и тот же CIL во всех случаях, и пусть бремя обработки типов с нулевым значением лежит в CLR.

Поведение CLR можно посмотреть в исходном коде coreclr на GitHub: IsInst, ObjIsInstanceOf. Как вы можете видеть во второй функции, если тип является нулевым представлением типа аргумента, он возвращает true.

разрешить приведение объекта типа T к Nullable (они имеют одинаковое представление)

Да, текущее поведение этих инструкций одинаков, поэтому изменение is T? на is T не будет иметь никакого значения (даже для аргумента null), но чтобы справиться с любыми возможными будущими изменениями в CLR, компилятор С# не может принять это решение (хотя вероятность изменения поведения isinst близка к нулю).

Nullable типы действительно замечательные вещи в .NET, особенно из-за их конкретной обработки в среде CLR, несмотря на то, что они не имеют специального синтаксиса в CIL (для совместимости). Фактически нет нормального способа боксирования типа с нулевым типом в его фактическом типе, а не в базовом, поскольку это приведет к возникновению несоответствий в приведениях и проверках (нулевая ссылка равна нулевому типу с нулевым значением или нет?). Тем не менее, вы можете обмануть CLR, подумав, что вы даете ему тип с нулевым значением (не то, что вам нужно).