Nota: esto parece haberse solucionado en Roslyn
Esta pregunta surgió cuando escribí mi respuesta a esta , que habla sobre la asociatividad del operador de fusión nula .
Solo como recordatorio, la idea del operador de fusió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
es no nulo,y
se no evaluado, y el valor dex
es el resultado final de la expresión, después de una conversión con el tipo de tiempo de compilación dey
si es necesario
Ahora, por lo general, no hay necesidad de una conversión, o es solo de un tipo anulable a uno no anulable; por lo general, los tipos son los mismos, o simplemente de (digamos) int?
a int
. Sin embargo, puede crear sus propios operadores de conversión implícitos, y estos se usan cuando es necesario.
Por el simple caso de x ?? y
, no he visto ningún comportamiento extraño. Sin embargo, con (x ?? y) ?? z
veo un comportamiento confuso.
Aquí hay un programa de prueba corto pero completo: los resultados están 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;
}
}
Entonces, 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 adicional de A a B en el primer caso? En particular, realmente hubiera esperado que el primer caso y el segundo caso fueran lo mismo: después de todo, es solo extraer una expresión en una variable local.
¿Algún tomador de lo que está pasando? Soy extremadamente reticente a gritar "error" cuando se trata del compilador de C #, pero estoy perplejo en cuanto a lo que está sucediendo ...
EDITAR: De acuerdo, aquí hay un ejemplo más desagradable de lo que está sucediendo, gracias a la respuesta del configurador, lo que me da más razones para pensar que es un error. EDITAR: La muestra ni siquiera necesita dos operadores de fusión nula 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;
}
}
El resultado de esto es:
Foo() called
Foo() called
A to int
El hecho de que me Foo()
llamen dos veces aquí me sorprende enormemente: no veo ninguna razón para que la expresión se evalúe dos veces.
fuente
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Obtendrá:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Respuestas:
Gracias a todos los que contribuyeron a analizar este problema. Es claramente un error del compilador. Parece suceder solo cuando hay una conversión elevada que involucra dos tipos anulables en el lado izquierdo del operador de fusión.
Todavía no he identificado dónde exactamente las cosas van mal, 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 del código, reducimos la expresión
del ejemplo anterior al equivalente moral de:
Claramente eso es incorrecto; la bajada correcta es
Mi mejor suposición basada en mi análisis hasta ahora es que el optimizador anulable se está descarrilando aquí. Tenemos un optimizador anulable que busca situaciones en las que sabemos que una expresión particular de tipo anulable no puede ser nula. Considere el siguiente análisis ingenuo: primero podríamos decir que
es lo mismo que
y entonces podríamos decir que
es lo mismo que
Pero el optimizador puede intervenir y decir "whoa, espera un minuto, ya verificamos que la temperatura no es nula; no hay necesidad de verificarlo por segunda vez solo porque estamos llamando a un operador de conversión elevado". Los optimizamos para que solo
Supongo 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 esa no es realmente la forma optimizada que queremos; queremos la forma optimizada de Foo () - reemplazado por temporal y luego convertido.Muchos errores en el compilador de C # son el resultado de malas decisiones de almacenamiento en caché. Una palabra para el sabio: cada vez que almacena un hecho en la memoria caché para usarlo más adelante, potencialmente 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 recuperación de un 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 probablemente fue mi error. ¡Lo siento!
Obtendré un error ingresado en la base de datos y veremos si podemos solucionarlo para una versión futura del idioma. Gracias nuevamente 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/
fuente
Esto definitivamente es un error.
Este código generará:
Eso me hizo pensar que la primera parte de cada
??
expresión de fusión se evalúa dos veces. Este código lo demostró:salidas:
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 como una cadena, y ninguna de ellas causó este comportamiento.
fuente
X() ?? Y()
expande internamente aX() != null ? X() : Y()
, por lo tanto, por qué se evaluaría dos veces.Si observa el código generado para el caso agrupado a la izquierda, en realidad hace algo como esto (
csc /optimize-
):Otro hallazgo, si se utiliza
first
va a generar un acceso directo si ambosa
yb
son nulos y de retornoc
. Sin embargo, sia
ob
no es nulo, se vuelve a evaluara
como parte de la conversión implícita aB
antes de devolver cuál dea
ob
no es nulo.De la especificación C # 4.0, §6.1.4:
Esto parece explicar la segunda combinación de desenvoltura y envoltura.
El compilador C # 2008 y 2010 produce 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:
Me pregunto si esto no se debe a la magia adicional dada al sistema de inferencia de tipos.
fuente
(x ?? y) ?? z
en lambdas anidadas, lo que garantiza una evaluación en orden sin doble evaluación. Obviamente, este no es el enfoque adoptado por el compilador de C # 4.0. Por lo que puedo decir, la sección 6.1.4 se aborda de una manera muy estricta en esta ruta de código particular y los temporales no se eluden, lo que resulta en una doble evaluación.En realidad, llamaré a esto un error ahora, con el ejemplo más claro. Esto aún se mantiene, pero la doble evaluación ciertamente no es buena.
Parece que
A ?? B
se implementa comoA.HasValue ? A : B
. En este caso, también hay mucho casting (siguiendo el casting regular para el?:
operador ternario ). Pero si ignora todo eso, entonces esto tiene sentido en función de cómo se implementa:A ?? B
se expande aA.HasValue ? A : B
A
es nuestrax ?? y
. Expandir ax.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Aquí puede ver que
x.HasValue
se verifica dos veces, y six ?? y
requiere lanzamiento, sex
lanzará dos veces.Lo mencionaría simplemente como un artefacto de cómoPara llevar: no cree operadores de lanzamiento implícitos con efectos secundarios.??
se implementa, en lugar de un error del compilador.Parece ser un error del compilador que gira en torno a cómo
??
se implementa. Para llevar: no anide expresiones de fusión con efectos secundarios.fuente
A() ? A() : B()
posiblemente evaluaráA()
dos veces, peroA() ?? B()
no tanto. Y dado que solo ocurre en el casting ... Hmm ... acabo de convencerme de que ciertamente no se está comportando correctamente.No soy un experto en C # en absoluto, como puede ver en mi historial de preguntas, pero probé esto y creo que es un error ... pero como novato, tengo que decir que no entiendo todo lo que sucede aquí, así que eliminaré mi respuesta si estoy lejos.
Llegué a esta
bug
conclusión al hacer una versión diferente de su programa que trata con el mismo escenario, pero mucho menos complicado.Estoy usando tres propiedades enteras nulas con almacenes de respaldo. Puse cada uno a 4 y luego corro
int? something2 = (A ?? B) ?? C;
( Código completo aquí )
Esto solo lee la A y nada más.
Esta declaración para mí me parece que debería:
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 y z no son todos nulos y, por lo tanto, esperaría que sean tratados de la misma manera que mi ejemplo menos complejo ... pero me temo que soy demasiado de un novato de C # y he perdido completamente el punto de esta pregunta.
fuente
int
). Empuja el caso aún más en un rincón oscuro al proporcionar múltiples conversiones de tipo implícito. Esto requiere que el compilador cambie el tipo de datos mientras se comparanull
. Debido a estas conversiones de tipo implícito, su ejemplo es diferente al suyo.