Curioso comportamiento de conversión implícito personalizado del operador de coalescencia nula


Nota: esto parece haber sido arreglado en Roslyn

Esta pregunta surgió al escribir mi respuesta a este, que habla de la asociatividad del operador de coalescencia nula.

Como recordatorio, la idea del operador coalescente nulo 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 null, y es no evaluados, y el valor de x es el resultado final de la expresión, después de una conversión al tipo en tiempo de compilación de y si es necesario

Ahora por lo general no hay necesidad de una conversión, o es solo de un tipo nullable a uno no nullable-generalmente los tipos son los mismos, o simplemente de (digamos) int? a int. Sin embargo, usted puede crear su propia conversión implícita operadores, y éstos se utilizan cuando es necesario.

Para 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;
    }
}

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

Puedo entender tanto el segundo caso como el tercer caso... pero ¿por qué es ¿hay una conversión extra de A a B en el primer caso? En particular, yo realmente habría esperado que el primer caso y el segundo caso fueran lo mismo - es solo extraer una expresión en una variable local, después de todo.

¿Alguna opinión sobre lo que está pasando? Soy extremadamente reacio a gritar " bug " cuando se trata del compilador de C#, pero estoy perplejo en cuanto a lo que está pasando...

EDITAR: Bien, aquí hay un ejemplo más desagradable de lo que está pasando, gracias a la respuesta del configurador, que da otra razón para pensar que es un bicho. EDITAR: La muestra ni siquiera necesita dos operadores coalescentes nulos 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 Foo() sea llamado dos veces aquí es enormemente sorprendente para mí - no puedo ver ninguna razón para que la expresión sea evaluada dos veces.

Author: Community, 2011-06-06

5 answers

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

Todavía no he identificado dónde precisamente las cosas van mal, pero en algún momento durante la fase de" reducción de nulos " de la compilación after después del análisis inicial pero antes de la generación de código reduce 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 nullable se está descarrilando aquí. Tenemos un optimizador nullable que busca situaciones donde sabemos que una expresión particular de tipo nullable no puede ser null. Consideremos el siguiente análisis ingenuo: podríamos decir primero que

result = Foo() ?? y;

Es lo mismo que

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

Y luego nos podría decirse 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 "whoa, espera un minuto, ya comprobamos que temp no es null; no hay necesidad de comprobarlo por null una segunda vez solo porque estamos llamando a un operador de conversión levantado". Los optimizamos a solo

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 que 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 el sabio: cada vez que cacheas un hecho para usarlo más tarde, estás potencialmente creando una inconsistencia si algo relevante cambia. En este caso, lo relevante que ha cambiado el análisis post inicial es que la llamada a Foo() siempre debe realizarse como un fetch de un temporal.

Hicimos mucha reorganización del paso de reescritura nullable 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 introducido en la base de datos y veremos si podemos arreglarlo para una versión futura del lenguaje. Gracias de nuevo a todos por su análisis; fue muy útil!

ACTUALIZACIÓN: Reescribí el optimizador nullable 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, ver mi serie de artículos que comienza aquí: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one /

 401
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-10 18:02:56

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 producirá:

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 ?? expresión de fusión 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 nullables; he intentado varias permutaciones con uno de los lados siendo una cadena, y ninguno de ellos causó este comportamiento.

 79
Author: configurator,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-07 14:33:00

Si echas un vistazo al código generado para el caso agrupado a la izquierda, en realidad 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 utiliza first generará un atajo si ambos a y b son null 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 nullable es de S? a T?:
    • Si el valor de origen es null (HasValue la propiedad es false), el resultado es el valor null del 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 una envoltura (§4.1.10) de T a T?.

Esto parece explicar el segundo desenvolver-envolver combinado.


El compilador C# 2008 y 2010 producen 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?

 51
Author: user7116,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-07 13:37:54

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

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

  1. A ?? B se expande a A.HasValue ? A : B
  2. A es nuestro x ?? y. Ampliar a x.HasValue : x ? y
  3. reemplazar todas las ocurrencias de A - > (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

Aquí puedes ver que x.HasValue se marca dos veces, y si x ?? y requiere fundición, x se lanzará dos veces.

Yo lo pondría simplemente como un artefacto de cómo ?? se implementa, en lugar de un error del compilador. Take-Away: No cree operadores de casting implícitos con efectos secundarios.

Parece ser un error del compilador que gira en torno a cómo se implementa ??. Para llevar: no anidar coalescente expresiones con efectos secundarios.

 14
Author: Philip Rieck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-06 23:51:27

No soy un experto en C# en absoluto como se 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 pasa aquí, así que voy a borrar mi respuesta si estoy lejos.

He llegado a esta bug conclusión haciendo una versión diferente de su programa que se ocupa del mismo escenario, pero mucho menos complicado.

Estoy usando tres propiedades de enteros nulos con almacenes de respaldo. Puse cada uno en 4 y luego ejecutar int? something2 = (A ?? B) ?? C;

(El código completo aquí )

Esto solo lee la A y nada más.

Esta declaración me parece que debería:

  1. Comience entre corchetes, mire A, devuelva A y termine si A no es null.
  2. Si A era nulo, evaluar B, terminar si B no es nulo
  3. Si A y B eran nulos, evalúe C.{[18]]}

Entonces, como A no es null, solo mira A A y termina.

En su ejemplo, poner un punto de interrupción en el El primer caso muestra que x, y y z no son nulos y, por lo tanto, esperaría que fueran tratados igual que mi ejemplo menos complejo.... ¡pero me temo que soy demasiado novato en C# y he perdido el punto de esta pregunta por completo!

 9
Author: Wil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-06 20:34:45