c# - net - Orden de argumento para ''=='' con Nullable<T>
int null c# (2)
Así que sentí curiosidad por la respuesta y eché un vistazo a la especificación c # 6 (no tengo idea de dónde está alojada la especificación c # 7). Descargo de responsabilidad completo: no garantizo que mi respuesta sea correcta, porque no escribí c # spec / compiler y mi comprensión de los aspectos internos es limitada.
Sin embargo, creo que la respuesta está en la respuesta del operador ==
recargable. La mejor sobrecarga aplicable para ==
se determina utilizando las reglas para mejores miembros de funciones .
De la especificación:
Dada una lista de argumentos A con un conjunto de expresiones de argumentos {E1, E2, ..., En} y dos miembros de función aplicables Mp y Mq con los tipos de parámetros {P1, P2, ..., Pn} y {Q1, Q2, ..., Qn}, Mp se define como un miembro de función mejor que Mq si
para cada argumento, la conversión implícita de Ex a Qx no es mejor que la conversión implícita de Ex a Px, y para al menos un argumento, la conversión de Ex a Px es mejor que la conversión de Ex a Qx.
Lo que me llamó la atención es la lista de argumentos {E1, E2, .., En}
. Si comparas un Nullable<bool>
con un bool
la lista de argumentos debería ser algo así como {Nullable<bool> a, bool b}
y para ese argumento, el Nullable<bool>.Equals(object o)
parece ser el mejor Función, porque solo toma una conversión implícita de bool
a object
.
Sin embargo, si revierte el orden de la lista de argumentos a {bool a, Nullable<bool> b}
the Nullable<bool>.Equals(object o)
ya no es la mejor función, porque ahora tendría que convertir de Nullable<bool>
a bool
en el primer argumento y luego de bool
a object
en el segundo argumento. Es por eso que para el caso A se selecciona una sobrecarga diferente que parece resultar en un código IL más limpio.
De nuevo, esta es una explicación que satisface mi propia curiosidad y parece estar en línea con la especificación de c #. Pero todavía tengo que averiguar cómo depurar el compilador para ver qué está pasando realmente.
Las siguientes dos funciones de C#
difieren solo en el intercambio del orden izquierdo / derecho de los argumentos al operador igual , ==
. (El tipo de IsInitialized
es bool
). Utilizando C # 7.1 y .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;
}
Pero el código IL para el segundo parece mucho más complejo. Por ejemplo, B es:
- 36 bytes más (código IL);
- llama a funciones adicionales incluyendo
newobj
yinitobj
; - declara cuatro locales contra uno solo.
IL para la función ''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 para la función ''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
Preguntas
- ¿Hay alguna diferencia de tiempo de ejecución funcional, semántica u otra sustancial entre A y B ? (Solo estamos interesados en la corrección aquí, no en el rendimiento)
- Si no son funcionalmente equivalentes, ¿cuáles son las condiciones de tiempo de ejecución que pueden exponer una diferencia observable?
- Si son equivalentes funcionales, ¿qué está haciendo B (que siempre termina con el mismo resultado que A ), y qué provocó su espasmo? ¿ B tiene ramas que nunca se pueden ejecutar?
- Si la diferencia se explica por la diferencia entre lo que aparece en el lado izquierdo de
==
, (aquí, una propiedad que hace referencia a una expresión frente a un valor literal), ¿puede indicar una sección de la especificación de C # que describa los detalles? - ¿Existe una regla de oro confiable que se pueda usar para predecir el IL hinchado en el momento de la codificación y así evitar su creación?
PRIMA. ¿Cómo se acumula el código final JITted x86
o AMD64
para cada pila?
[editar]
Notas adicionales basadas en comentarios en los comentarios. Primero, se propuso una tercera variante, pero da una IL idéntica a A (para las compilaciones Debug
y Release
). Sin embargo, silísticamente, el C # para el nuevo parece más elegante que A :
static void C(ISupportInitialize x)
{
if ((x as ISupportInitializeNotification)?.IsInitialized ?? false)
throw null;
}
Aquí también está el Release
IL para cada función. Tenga en cuenta que la asimetría A / C vs. B todavía es evidente con la Release
IL, por lo que la pregunta original sigue en pie.
Suelte IL para las funciones ''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
Suelte IL para la función ''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
Finalmente, se mencionó una versión que usa la nueva sintaxis de C # 7 que parece producir la IL más limpia de todas:
static void D(ISupportInitialize x)
{
if (x is ISupportInitializeNotification y && y.IsInitialized)
throw null;
}
Suelte IL para la función ''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
Parece que el primer operando se convierte al tipo del segundo con el propósito de comparación.
Las operaciones en exceso en el caso B implican la construcción de un Nullable<bool>(true)
. Mientras que en el caso A, para comparar algo con true
/ false
, hay una sola instrucción IL ( brfalse.s
) que lo hace.
No pude encontrar la referencia específica en la especificación C # 5.0 . 7.10 Los operadores relacionales y de prueba de tipo se refieren a 7.3.4 Resolución de sobrecarga del operador binario que a su vez se refiere a 7.5.3 Resolución de sobrecarga , pero el último es muy vago.