¿Por qué las estructuras genéricas y no genéricas se tratan de manera diferente cuando se construye una expresión que eleva operator == a nullable?


Esto parece un error en la elevación a null de operandos en estructuras genéricas.

Considere la siguiente estructura ficticia, que anula operator==:

struct MyStruct
{
    private readonly int _value;
    public MyStruct(int val) { this._value = val; }

    public override bool Equals(object obj) { return false; }
    public override int GetHashCode() { return base.GetHashCode(); }

    public static bool operator ==(MyStruct a, MyStruct b) { return false; }
    public static bool operator !=(MyStruct a, MyStruct b) { return false; }
}

Ahora considere las siguientes expresiones:

Expression<Func<MyStruct, MyStruct, bool>> exprA   = 
    (valueA, valueB) => valueA == valueB;

Expression<Func<MyStruct?, MyStruct?, bool>> exprB = 
    (nullableValueA, nullableValueB) => nullableValueA == nullableValueB;

Expression<Func<MyStruct?, MyStruct, bool>> exprC  = 
    (nullableValueA, valueB) => nullableValueA == valueB;

Los tres compilan y se ejecutan como se espera.

Cuando se compilan (usando .Compile()) producen el siguiente código (parafraseado al inglés de la IL):

  1. La primera expresión que toma solo MyStruct (no es nullable) args, simplemente llama op_Equality (nuestra implementación de operator ==)

  2. La segunda expresión, cuando se compila, produce código que comprueba cada argumento para ver si HasValue. Si ambos no lo hacen (ambos iguales null), devuelve true. Si solo uno tiene un valor, devuelve false. De lo contrario, llama op_Equality a los dos valores.

  3. La tercera expresión comprueba el argumento nullable para ver si tiene un valor - si no, devuelve false. De lo contrario, llama op_Equality.

Hasta ahora todo bien.

Siguiente paso: haga exactamente lo mismo con un tipo genérico: cambie MyStruct a MyStruct<T> en todas partes en la definición del tipo, y cámbielo a MyStruct<int> en las expresiones.

Ahora la tercera expresión compila pero lanza una excepción de tiempo de ejecución InvalidOperationException con el siguiente mensaje:

Los operandos para el operador 'Equal' no coinciden con los parámetros del método 'op_Equality'.

Esperaría que las estructuras genéricas se comportaran exactamente igual que las no genéricas, con todos los nullable-elevación descrita anteriormente.

Así que mis preguntas son:

  1. ¿Por qué hay una diferencia entre estructuras genéricas y no genéricas?
  2. ¿Cuál es el significado de esta excepción?
  3. ¿Es esto un error en C#/. NET?

El código completo para reproducir esto está disponible en esta síntesis.

Author: sinelaw, 2013-05-28

2 answers

La respuesta corta es: sí, eso es un error. He puesto un repro mínimo y un breve análisis a continuación.

Mis disculpas. Escribí mucho de ese código, así que probablemente fue mi culpa.

He enviado un repro a los equipos de desarrollo, prueba y gestión de programas de Roslyn. Dudo que esto se reproduzca en Roslyn, pero verificarán que no lo hace y decidirán si esto hace la barra para un service pack de C# 5.

Siéntase libre de ingresar un problema en connect.microsoft.com también si quieres rastreó allí también.


Reproducción mínima:

using System;
using System.Linq.Expressions;
struct S<T>
{
    public static bool operator ==(S<T> a, S<T> b) { return false; }
    public static bool operator !=(S<T> a, S<T> b) { return false; }
}
class Program
{
    static void Main()
    {
        Expression<Func<S<int>?, S<int>, bool>> x = (a, b) => a == b;
    }
}

El código que se genera en la reproducción mínima es equivalente a

ParameterExpression pa = Expression.Parameter(typeof(S<int>?), "a");
ParameterExpression pb = Expression.Parameter(typeof(S<int>), "b");
Expression.Lambda<Func<S<int>?, S<int>, bool>>(
    Expression.Equal(pa, pb, false, infoof(S<int>.op_Equality)
    new ParameterExpression[2] { pa, pb } );

Donde infoof es un operador falso que obtiene un MethodInfo para el método dado.

El código correcto sería:

ParameterExpression pa = Expression.Parameter(typeof(S<int>?), "a");
ParameterExpression pb = Expression.Parameter(typeof(S<int>), "b");
Expression.Lambda<Func<S<int>?, S<int>, bool>>(
    Expression.Equal(pa, Expression.Convert(pb, typeof(S<int>?), false, infoof(S<int>.op_Equality)
    new ParameterExpression[2] { pa, pb } );

El método Equal no puede tratar con un operandos nullable, uno no nullable. Requiere que ambos sean nullables o que ninguno lo sea.

(Tenga en cuenta que el false es correcto. Este Booleano controla si el resultado de una igualdad elevada es un booleano elevado; en C# no lo es, en VB lo es.)

 22
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
2013-05-28 17:51:36

Sí, este error se ha ido en Roslyn (el compilador en desarrollo). Veremos sobre el producto existente.

 5
Author: Neal Gafter,
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
2013-05-28 22:05:33