Почему компилятор С# переводит это!= Сравнение, как если бы это было сравнение?

Я по чистой случайности обнаружил, что компилятор С# обращается к этому методу:

static bool IsNotNull(object obj)
{
    return obj != null;
}

... в этот CIL:

.method private hidebysig static bool IsNotNull(object obj) cil managed
{
    ldarg.0   // obj
    ldnull
    cgt.un
    ret
}

... или, если вы предпочитаете смотреть на декомпилированный код С#:

static bool IsNotNull(object obj)
{
    return obj > null;   // (note: this is not a valid C# expression)
}

Как получилось, что != переводится как ">"?

Ответ 1

Краткий ответ:

В IL нет инструкции "сравнивать не равные", поэтому оператор С# != не имеет точного соответствия и не может быть переведен буквально.

Однако существует "сравниваемая поровну" инструкция (ceq, прямое соответствие оператору ==), поэтому в общем случае x != y переводится как его несколько более длинный эквивалент (x == y) == false.

В IL (cgt) имеется команда "compare-more-than", которая позволяет компилятору принимать определенные ярлыки (т.е. генерировать более короткий IL-код), один из которых заключается в сравнении неравенств объектов с нулем, obj != null, переводится так, как будто они были "obj > null".


Давайте рассмотрим более подробно.

Если в IL нет инструкции "сравните-не-равно", то как будет преобразован следующий метод компилятором?

static bool IsNotEqual(int x, int y)
{
    return x != y;
}

Как уже было сказано выше, компилятор превратит x != y в (x == y) == false:

.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed 
{
    ldarg.0   // x
    ldarg.1   // y
    ceq
    ldc.i4.0  // false
    ceq       // (note: two comparisons in total)
    ret
}

Оказывается, что компилятор не всегда производит этот довольно длинный шаблон. Посмотрим, что произойдет, когда мы заменим y на константу 0:

static bool IsNotZero(int x)
{
    return x != 0;
}

Произведенный IL несколько короче, чем в общем случае:

.method private hidebysig static bool IsNotZero(int32 x) cil managed 
{
    ldarg.0    // x
    ldc.i4.0   // 0
    cgt.un     // (note: just one comparison)
    ret
}

Компилятор может воспользоваться тем фактом, что целые числа со знаком хранятся в двух дополнениях (где, если результирующие битовые шаблоны интерпретируются как целые числа без знака — .un означает — 0 имеет наименьшее возможное значение), поэтому он переводит x == 0, как если бы он был unchecked((uint)x) > 0.

Оказывается, компилятор может сделать то же самое для проверок неравенства в отношении null:

static bool IsNotNull(object obj)
{
    return obj != null;
}

Компилятор производит почти тот же IL, что и для IsNotZero:

.method private hidebysig static bool IsNotNull(object obj) cil managed 
{
    ldarg.0
    ldnull   // (note: this is the only difference)
    cgt.un
    ret
}

По-видимому, компилятору разрешено предположить, что бит-шаблон ссылки null является наименьшим битовым шаблоном, возможным для любой ссылки на объект.

Этот ярлык явно упоминается в стандартном аннотированном стандарте языковой инфраструктуры (1-е издание с октября 2003 года) (на странице 491, в качестве сноски к Таблице 6-4, "Бинарные сравнения или операции ветки" ):

"cgt.un разрешен и проверен в ObjectRefs (O). Это обычно используется при сравнении ObjectRef с нулевым значением (нет инструкции" сравните-не-равно ", которая в противном случае была бы более очевидным решением)."