variable net bool asignar c# nullable equals-operator commutativity

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 y initobj ;
  • 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

  1. ¿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)
  2. Si no son funcionalmente equivalentes, ¿cuáles son las condiciones de tiempo de ejecución que pueden exponer una diferencia observable?
  3. 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?
  4. 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?
  5. ¿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.