Порядок аргумента для '==' с Nullable<T>
Следующие два C#
функции отличаются только переключением левого / правого порядка аргументов на оператор равенства, ==
, (Тип IsInitialized
является bool
). Использование C# 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 ветки, которые никогда не могут выполняться?
- Если разница объясняется разницей между тем, что появляется на левой стороне
==
(здесь свойство, ссылающееся на выражение, а не на буквальное значение), можете ли вы указать раздел спецификации C#, который описывает детали. - Существует ли надежное эмпирическое правило, которое можно использовать для предсказания раздутого IL во время кодирования и, таким образом, избежать его создания?
БОНУС. Как выглядит соответствующий финал JITted x86
или же AMD64
код для каждого стека?
[редактировать]
Дополнительные заметки основаны на отзывах в комментариях. Сначала был предложен третий вариант, но он дает идентичный IL как A (для обоих Debug
а также Release
строит). Однако, по-новому, C# для нового кажется более гладким, чем A:
static void C(ISupportInitialize x)
{
if ((x as ISupportInitializeNotification)?.IsInitialized ?? false)
throw null;
}
Здесь также есть Release
IL для каждой функции. Обратите внимание, что асимметрия A/C и B по-прежнему очевидна с 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
Наконец, была упомянута версия, использующая новый синтаксис C# 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
2 ответа
Похоже, 1-й операнд преобразуется во 2-й тип для сравнения.
Избыточные операции в случае B включают построение Nullable<bool>(true)
, Хотя в случае А, сравнить что-то с true
/false
есть одна инструкция IL (brfalse.s
) это делает
Я не смог найти конкретную ссылку в спецификации C# 5.0. 7.10 Операторы реляционного и типового тестирования относятся к 7.3.4 Разрешение перегрузки бинарных операторов, которое, в свою очередь, относится к 7.5.3 разрешению перегрузки, но последнее очень расплывчато.
Поэтому мне было любопытно получить ответ и взглянуть на спецификацию C# 6 (без понятия, где находится спецификация C# 7). Полный отказ от ответственности: я не гарантирую, что мой ответ правильный, потому что я не писал c/ spec/ compiler и мое понимание внутренних возможностей ограничено.
Тем не менее, я думаю, что ответ заключается в разрешении перегрузки ==
оператор. Лучшая применимая перегрузка для ==
определяется с помощью правил для лучших членов функции.
Из спецификации:
Имеется список аргументов 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>
к 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-коду.
Опять же, это объяснение, которое удовлетворяет мое собственное любопытство и похоже соответствует спецификации C#. Но мне еще предстоит выяснить, как отладить компилятор, чтобы увидеть, что на самом деле происходит.