Порядок аргументов для '==' с Nullable <T>

Следующие две функции C# отличаются только заменой порядка аргументов влево/вправо оператору equals, ==. (Тип IsInitialized - bool). Использование С# 7.1 и .NET 4.7.

static void A(ISupportInitialize x)
{
    if ((x as ISupportInitializeNotification)?.IsInitialized == true)
        throw null;
}

static void B(ISupportInitialize x)
{
    if (true == (x as ISupportInitializeNotification)?.IsInitialized)
        throw null;
}

Но код IL для второго выглядит намного сложнее. Например, B:

  • 36 байт дольше (код IL);
  • вызывает дополнительные функции, включая newobj и initobj;
  • объявляет четырех локальных жителей по сравнению с одним.

IL для функции 'A'...

[0] bool flag
        nop
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_000e
        pop
        ldc.i4.0
        br.s L_0013
L_000e: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
L_0013: stloc.0
        ldloc.0
        brfalse.s L_0019
        ldnull
        throw
L_0019: ret

IL для функции 'B'...

[0] bool flag,
[1] bool flag2,
[2] valuetype [mscorlib]Nullable`1<bool> nullable,
[3] valuetype [mscorlib]Nullable`1<bool> nullable2
        nop
        ldc.i4.1
        stloc.1
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_0018
        pop
        ldloca.s nullable2
        initobj [mscorlib]Nullable`1<bool>
        ldloc.3
        br.s L_0022
L_0018: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0022: stloc.2
        ldloc.1
        ldloca.s nullable
        call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
        beq.s L_0030
        ldc.i4.0
        br.s L_0037
L_0030: ldloca.s nullable
        call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0037: stloc.0
        ldloc.0
        brfalse.s L_003d
        ldnull
        throw
L_003d: ret

 

Quesions

  • Есть ли функциональная, семантическая или другая существенная разница во времени между A и B? (Нас интересует только правильность, а не производительность).
  • Если они не являются функционально эквивалентными, каковы условия выполнения, которые могут выявить наблюдаемую разницу?
  • Если они являются функциональными эквивалентами, что делает B (что всегда заканчивается тем же результатом, что и A), и что вызвало его спазм? Имеет ли B ветки, которые никогда не могут выполняться?
  • Если разница объясняется разницей между тем, что появляется на стороне слева == (здесь выражение привязки свойства к буквенному значению), вы можете указать раздел С# spec, который описывает детали.
  • Есть ли надежное правило большого пальца, которое можно использовать для прогнозирования раздутого IL во время кодирования и, таким образом, избежать его создания?

    БОНУС. Каким образом соответствующий конечный код JITted x86 или AMD64 для каждого стека?


[править]
Дополнительные примечания, основанные на отзывах в комментариях. Сначала был предложен третий вариант, но он дает идентичный IL как A (для обоих Debug и Release строит). Однако силистически С# для нового кажется более гладким, чем A:

static void C(ISupportInitialize x)
{
    if ((x as ISupportInitializeNotification)?.IsInitialized ?? false)
        throw null;
}

Здесь также есть Release IL для каждой функции. Обратите внимание, что асимметрия A/ C и B по-прежнему очевидна с IL Release, поэтому исходный вопрос все еще стоит.

Выпуск IL для функций "A", "C"...

        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_000d
        pop
        ldc.i4.0
        br.s L_0012
L_000d: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        brfalse.s L_0016
        ldnull
        throw
L_0016: ret

Отпустить IL для функции 'B'...

[0] valuetype [mscorlib]Nullable`1<bool> nullable,
[1] valuetype [mscorlib]Nullable`1<bool> nullable2
        ldc.i4.1
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_0016
        pop
        ldloca.s nullable2
        initobj [mscorlib]Nullable`1<bool>
        ldloc.1
        br.s L_0020
L_0016: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0020: stloc.0
        ldloca.s nullable
        call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
        beq.s L_002d
        ldc.i4.0
        br.s L_0034
L_002d: ldloca.s nullable
        call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0034: brfalse.s L_0038
        ldnull
        throw
L_0038: ret

Наконец, была упомянута версия с использованием нового синтаксиса С# 7, который, по-видимому, производит самый чистый IL из всех:

static void D(ISupportInitialize x)
{
    if (x is ISupportInitializeNotification y && y.IsInitialized)
        throw null;
}

Выпуск IL для функции 'D'...

[0] class [System]ISupportInitializeNotification y
        ldarg.0 
        isinst [System]ISupportInitializeNotification
        dup 
        stloc.0 
        brfalse.s L_0014
        ldloc.0 
        callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        brfalse.s L_0014
        ldnull 
        throw 
L_0014: ret 

Ответ 1

Похоже, что 1-й операнд преобразуется во 2-й тип для сравнения.

Излишние операции в случае B включают построение a Nullable<bool>(true). Хотя в случае A, чтобы сравнить что-то с true/false, есть одна команда IL (brfalse.s), которая делает это.

Я не смог найти конкретную ссылку в С# 5.0 spec. 7.10 Операторы реляционного и типового тестирования относятся к 7.3.4. Разрешение перегрузки двоичных операторов, которое, в свою очередь, относится к 7.5.3. Разрешение перегрузки, но последний очень расплывчатый.

Ответ 2

Поэтому мне было интересно узнать ответ и взглянуть на спецификацию С# 6 (нет подсказки, где размещается спецификация С# 7). Полное отказ от ответственности: я не гарантирую, что мой ответ верен, потому что я не писал спецификатор С# или компилятор, и мое понимание внутренних компонентов ограничено.

Тем не менее, я думаю, что ответ заключается в результате перезагрузки ==. Наилучшая применимая перегрузка для == определяется с помощью правил для лучших членов функции.

Из спецификации:

Учитывая список аргументов A с набором выражений аргументов {E1, E2,..., En} и два применимых члена функции Mp и Mq с параметром типы {P1, P2,..., Pn} и {Q1, Q2,..., Qn}, Mp определяется как лучший член функции, чем Mq, если

для каждого аргумента, неявное преобразование из Ex в Qx не лучше чем неявное преобразование из Ex в Px, и, по крайней мере, для одного аргумент, преобразование из Ex в Px лучше, чем преобразование от Ex до Qx.

То, что привлекло мое внимание, это список аргументов {E1, E2, .., En}. Если вы сравниваете Nullable<bool> с a bool, список аргументов должен быть чем-то вроде {Nullable<bool> a, bool b}, и для этого списка аргументов метод Nullable<bool>.Equals(object o) представляется лучшей функцией, поскольку он принимает только одно неявное преобразование из bool до object.

Однако, если вы вернете порядок списка аргументов в {bool a, Nullable<bool> b}, метод Nullable<bool>.Equals(object o) больше не является лучшей функцией, потому что теперь вам придется преобразовать из Nullable<bool> в bool в первый аргумент, а затем от bool до object во втором аргументе. Поэтому для случая A выбрана другая перегрузка, которая, как представляется, приводит к более чистым IL-кодам.

Опять же, это объяснение, которое удовлетворяет моему собственному любопытству и, похоже, соответствует спецификации С#. Но мне еще предстоит выяснить, как отлаживать компилятор, чтобы узнать, что происходит на самом деле.