question operator operador mark empty diferente c# null-coalescing-operator

c# - operator - Curioso comportamiento nulo coalescente de conversión implícita personalizada del operador



isnull c# (5)

Nota: esto parece haber sido arreglado en Roslyn

Esta pregunta surgió cuando escribí mi respuesta a esta , que habla sobre la asociatividad del operador de unión nula .

Solo como un recordatorio, la idea del operador de unión nula es que una expresión de la forma

x ?? y

Primero evalúa x , luego:

  • Si el valor de x es nulo, y se evalúa y ese es el resultado final de la expresión
  • Si el valor de x no es nulo, y no se evalúa, y el valor de x es el resultado final de la expresión, después de una conversión al tipo de tiempo de compilación de y si es necesario

Ahora generalmente no hay necesidad de una conversión, o es solo de un tipo anulable a uno no anulable - generalmente los tipos son los mismos, o solo de (digamos) int? a int . Sin embargo, puede crear sus propios operadores de conversión implícitos, y esos se utilizan cuando sea necesario.

Para el caso simple de x ?? y x ?? y , no he visto ningún comportamiento extraño. Sin embargo, con (x ?? y) ?? z (x ?? y) ?? z veo un comportamiento confuso

Este es un programa de prueba breve pero completo. Los resultados se encuentran en los comentarios:

using System; public struct A { public static implicit operator B(A input) { Console.WriteLine("A to B"); return new B(); } public static implicit operator C(A input) { Console.WriteLine("A to C"); return new C(); } } public struct B { public static implicit operator C(B input) { Console.WriteLine("B to C"); return new C(); } } public struct C {} class Test { static void Main() { A? x = new A(); B? y = new B(); C? z = new C(); C zNotNull = new C(); Console.WriteLine("First case"); // This prints // A to B // A to B // B to C C? first = (x ?? y) ?? z; Console.WriteLine("Second case"); // This prints // A to B // B to C var tmp = x ?? y; C? second = tmp ?? z; Console.WriteLine("Third case"); // This prints // A to B // B to C C? third = (x ?? y) ?? zNotNull; } }

Así que tenemos tres tipos de valores personalizados, A , B y C , con conversiones de A a B, A a C y B a C.

Puedo entender tanto el segundo caso como el tercer caso ... pero ¿por qué hay una conversión extra de A a B en el primer caso? En particular, realmente esperaba que el primer caso y el segundo caso fueran lo mismo: después de todo, simplemente está extrayendo una expresión en una variable local.

¿Algún tomador de lo que está pasando? Soy extremadamente reacio a gritar "error" cuando se trata del compilador de C #, pero estoy sorprendido de lo que está pasando ...

EDIT: Bueno, aquí hay un ejemplo más desagradable de lo que está pasando, gracias a la respuesta del configurador, lo que me da una razón más para pensar que es un error. EDIT: La muestra ni siquiera necesita dos operadores nulos que se unen ahora ...

using System; public struct A { public static implicit operator int(A input) { Console.WriteLine("A to int"); return 10; } } class Test { static A? Foo() { Console.WriteLine("Foo() called"); return new A(); } static void Main() { int? y = 10; int? result = Foo() ?? y; } }

La salida de esto es:

Foo() called Foo() called A to int

El hecho de que llamen a Foo() dos veces aquí me sorprende enormemente; no veo ninguna razón para que la expresión se evalúe dos veces.


En realidad, llamaré a esto un error ahora, con el ejemplo más claro. Esto sigue siendo válido, pero la doble evaluación ciertamente no es buena.

Parece como si A ?? B A ?? B se implementa como A.HasValue ? A : B A.HasValue ? A : B En este caso, también hay mucho casting (¿sigue el casting regular para el operador ternario ?: :). Pero si ignoras todo eso, entonces esto tiene sentido en función de cómo se implementa:

  1. A ?? B A ?? B expande a A.HasValue ? A : B A.HasValue ? A : B
  2. A es nuestro x ?? y x ?? y Expandir a x.HasValue : x ? y x.HasValue : x ? y
  3. Reemplace todas las apariciones de A -> (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

Aquí puede ver que x.HasValue se comprueba dos veces, y si x ?? y x ?? y requiere un lanzamiento, x será lanzado dos veces.

¿Lo habría puesto simplemente como un artefacto de cómo ?? Se implementa, en lugar de un error de compilación. Para llevar: no cree operadores de fundición implícitos con efectos secundarios.

Parece ser un error de compilación que gira en torno a cómo ?? está implementado. Para llevar: no anide expresiones coalescentes con efectos secundarios.


Esto es definitivamente un error.

public class Program { static A? X() { Console.WriteLine("X()"); return new A(); } static B? Y() { Console.WriteLine("Y()"); return new B(); } static C? Z() { Console.WriteLine("Z()"); return new C(); } public static void Main() { C? test = (X() ?? Y()) ?? Z(); } }

Este código dará salida:

X() X() A to B (0) X() X() A to B (0) B to C (0)

Eso me hizo pensar que la primera parte de cada uno ?? La expresión coalesce se evalúa dos veces. Este código lo demostró:

B? test= (X() ?? Y());

salidas:

X() X() A to B (0)

Esto parece suceder solo cuando la expresión requiere una conversión entre dos tipos anulables; He intentado varias permutaciones con uno de los lados siendo una cadena, y ninguno de ellos causó este comportamiento.


Gracias a todos los que contribuyeron a analizar este tema. Es claramente un error de compilación. Parece que solo ocurre cuando hay una conversión elevada que involucra dos tipos anulables en el lado izquierdo del operador coalescente.

Todavía no he identificado dónde precisamente van mal las cosas, pero en algún momento durante la fase de compilación de "reducción anulable", después del análisis inicial pero antes de la generación de código, reducimos la expresión

result = Foo() ?? y;

Del ejemplo anterior al equivalente moral de:

A? temp = Foo(); result = temp.HasValue ? new int?(A.op_implicit(Foo().Value)) : y;

Claramente eso es incorrecto; la bajada correcta es

result = temp.HasValue ? new int?(A.op_implicit(temp.Value)) : y;

Mi mejor conjetura basada en mi análisis hasta ahora es que el optimizador de valores nulos se está descarrilando aquí. Tenemos un optimizador de nulos que busca situaciones en las que sabemos que una expresión particular de tipo nullable no puede ser nula. Considere el siguiente análisis ingenuo: primero podríamos decir que

result = Foo() ?? y;

es lo mismo que

A? temp = Foo(); result = temp.HasValue ? (int?) temp : y;

y entonces podríamos decir que

conversionResult = (int?) temp

es lo mismo que

A? temp2 = temp; conversionResult = temp2.HasValue ? new int?(op_Implicit(temp2.Value)) : (int?) null

Pero el optimizador puede intervenir y decir: "espera, un minuto, ya verificamos que la temperatura no es nula; no hay necesidad de comprobarlo por segunda vez solo porque estamos llamando a un operador de conversión elevado". Les habríamos optimizado para que se fuera.

new int?(op_Implicit(temp2.Value))

Mi conjetura es que estamos en algún lugar almacenando en caché el hecho de que la forma optimizada de (int?)Foo() es new int?(op_implicit(Foo().Value)) pero en realidad no es la forma optimizada que queremos; queremos la forma optimizada de Foo () - reemplazado con temporal y luego convertido.

Muchos errores en el compilador de C # son el resultado de malas decisiones de almacenamiento en caché. Una palabra para los sabios: cada vez que almacene en caché un hecho para su uso posterior, posiblemente esté creando una inconsistencia en caso de que algo relevante cambie . En este caso, lo relevante que ha cambiado después del análisis inicial es que la llamada a Foo () siempre debe realizarse como una búsqueda temporal.

Hicimos mucha reorganización del pase de reescritura anulable en C # 3.0. El error se reproduce en C # 3.0 y 4.0 pero no en C # 2.0, lo que significa que el error fue probablemente mi error. ¡Lo siento!

Obtendré un error ingresado en la base de datos y veremos si podemos arreglarlo para una versión futura del idioma. Gracias de nuevo a todos por su análisis; fue muy útil!

ACTUALIZACIÓN: reescribí el optimizador anulable desde cero para Roslyn; ahora hace un mejor trabajo y evita este tipo de errores extraños. Para algunas ideas sobre cómo funciona el optimizador en Roslyn, vea mi serie de artículos que comienza aquí: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/


No soy un experto en C #, como puede ver en mi historial de preguntas, pero probé esto y creo que es un error ... pero como novato, debo decir que no entiendo todo lo que sucede. aquí, así que eliminaré mi respuesta si estoy lejos.

He llegado a esta conclusión de bug al hacer una versión diferente de su programa que trata el mismo escenario, pero mucho menos complicado.

Estoy utilizando tres propiedades enteras nulas con almacenes de respaldo. Pongo cada uno en 4 y luego ejecuto int? something2 = (A ?? B) ?? C; int? something2 = (A ?? B) ?? C;

( Código completo aquí )

Esto solo lee la A y nada más.

Para mí, esta declaración me parece que debería:

  1. Comience entre corchetes, mire A, regrese A y termine si A no es nulo.
  2. Si A era nulo, evalúe B, finalice si B no es nulo
  3. Si A y B fueran nulos, evalúe C.

Entonces, como A no es nulo, solo mira a A y termina.

En su ejemplo, poner un punto de interrupción en el Primer Caso muestra que x, y, z, no son todos nulos y, por lo tanto, espero que sean tratados igual que mi ejemplo menos complejo ... pero me temo que soy demasiado de un novato en C # y me he perdido el punto de esta pregunta por completo!


Si observa el código generado para el caso agrupado por la izquierda, realmente hace algo como esto ( csc /optimize- ):

C? first; A? atemp = a; B? btemp = (atemp.HasValue ? new B?(a.Value) : b); if (btemp.HasValue) { first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value); }

Otro hallazgo, si usa first , generará un acceso directo si a y b son nulos y devuelven c . Sin embargo, si a o b no es nulo, vuelve a evaluar a como parte de la conversión implícita a B antes de devolver cuál de a o b no es nulo.

De la especificación C # 4.0, §6.1.4:

  • Si la conversión anulable es de S? a T? :
    • Si el valor de origen es null (la propiedad HasValue es false ), el resultado es el valor null de tipo T? .
    • De lo contrario, la conversión se evalúa como un desenvolvimiento de S? a S , seguido de la conversión subyacente de S a T , seguido de un ajuste (§4.1.10) de T a T? .

Esto parece explicar la segunda combinación de desempaquetar y envolver.

El compilador C # 2008 y 2010 producen un código muy similar, sin embargo, esto parece una regresión del compilador C # 2005 (8.00.50727.4927) que genera el siguiente código para lo anterior:

A? a = x; B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y; C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

Me pregunto si esto no se debe a la magia adicional dada al sistema de inferencia de tipos.