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

147

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

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
}

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

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

Почему это !=переводится как " >"?

Stakx - больше не помогает
источник

Ответы:

201

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

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

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

В IL ( ) есть также инструкция «сравнить больше, чем», cgtкоторая позволяет компилятору использовать определенные комбинации клавиш (т. Е. Генерировать более короткий код 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;
}

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

.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 с нулевым значением (нет инструкции« сравнить-не-равно », которая в противном случае была бы более очевидным решением).»

Stakx - больше не помогает
источник
3
Отличный ответ, только одна гнида: два дополнения здесь не актуальны. Имеет значение только то, что целые числа со знаком хранятся таким образом, что неотрицательные значения в intдиапазоне имеют такое же представление, intкак и в uint. Это гораздо более слабое требование, чем дополнение к двум.
3
Беззнаковые типы никогда не имеют отрицательных чисел, поэтому операция сравнения, которая сравнивается с нулем, не может обрабатывать любое ненулевое число как меньшее, чем ноль. Все представления, соответствующие неотрицательным значениям int, уже заняты одним и тем же значением в uint, поэтому все представления, соответствующие отрицательным значениям, intдолжны соответствовать некоторому значению uintбольше, чем 0x7FFFFFFF, но на самом деле не имеет значения, какое значение этого является. ( На самом деле, все , что действительно требуется, что ноль представляется одинаково в обоих intи uint.)
3
@hvd: Спасибо за объяснение. Вы правы, это не два дополнения, которые имеют значение; это требование, которое вы упомянули, и тот факт, что он cgt.unобрабатывает intкак uintбез изменения базовый битовый шаблон. (Представьте себе , что cgt.unбы сначала попытаться исправить потери значимости путем сопоставления всех отрицательных чисел в 0. В этом случае вы , очевидно , не может заменить > 0для != 0.)
stakx - больше не способствующего
2
Я нахожу удивительным, что сравнение ссылки на объект с другой, использующей >проверяемый IL. Таким образом, можно сравнить два ненулевых объекта и получить логический результат (который не является детерминированным). Это не проблема безопасности памяти, но похоже на нечистую конструкцию, которая не соответствует общему духу безопасного управляемого кода. В этом проекте прослеживается тот факт, что ссылки на объекты реализованы в виде указателей. Похоже, недостаток дизайна .NET CLI.
USR
3
@usr: Абсолютно! В разделе III.1.1.4 стандарта CLI говорится, что «ссылки на объекты (тип O) полностью непрозрачны» и что «единственные допустимые операции сравнения - это равенство и неравенство…». Возможно , потому что ссылки на объект не определены в терминах адресов памяти, стандарт также берет на себя концептуально сохранить ссылку нулевой обособлен от 0 (см , например , определение ldnull, initobjи newobj). Таким образом, использование cgt.unсравнения объектов с нулевой ссылкой противоречит разделу III.1.1.4 более чем одним способом.
stakx - больше не вносит свой вклад