Почему общие и неуниверсальные структуры обрабатываются по-разному при построении выражения, которое поднимает оператор == до нуля?

Это похоже на ошибку при поднятии до нуля операндов на общих структурах.

Рассмотрим следующую фиктивную структуру, которая переопределяет 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; }
}

Теперь рассмотрим следующие выражения:

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;

Все три компилируются и запускаются, как и ожидалось.

Когда они скомпилированы (используя .Compile()) они производят следующий код (перефразированный на английский из IL):

  1. Первое выражение, которое принимает только MyStruct (не обнуляемый) args, просто вызывает op_Equality (наша реализация operator ==)

  2. Второе выражение, когда скомпилировано, производит код, который проверяет каждый аргумент, чтобы увидеть, если он HasValue, Если оба не (оба равны null), возвращает true, Если только один имеет значение, возвращает false, В противном случае звонки op_Equality на двух значениях.

  3. Третье выражение проверяет обнуляемый аргумент, чтобы увидеть, имеет ли оно значение - если нет, возвращает false. В противном случае звонки op_Equality,

Все идет нормально.

Следующий шаг: сделать то же самое с универсальным типом - изменить MyStruct в MyStruct<T> везде в определении типа, и измените его на MyStruct<int> в выражениях.

Теперь третье выражение компилируется, но выдает исключение времени выполнения InvalidOperationException со следующим сообщением:

Операнды для оператора "Равный" не соответствуют параметрам метода "op_Equality".

Я ожидал бы, что родовые структуры будут вести себя точно так же, как неуниверсальные, со всеми отменяемыми значениями, описанными выше.

Итак, мои вопросы:

  1. Почему существует разница между общими и неуниверсальными структурами?
  2. В чем смысл этого исключения?
  3. Это ошибка в C#/.NET?

Полный код для воспроизведения этого доступен в этой сути.

2 ответа

Решение

Краткий ответ: да, это ошибка. Я поместил минимальное воспроизведение и краткий анализ ниже.

Мои извенения. Я написал много этого кода, и, вероятно, это было плохо.

Я отправил репродукцию команде разработчиков, тестировщиков и менеджеров программ Roslyn. Я сомневаюсь, что это воспроизводится в Roslyn, но они проверят, что это не так, и решат, делает ли это планку для пакета обновления C# 5.

Не стесняйтесь также указывать проблему на connect.microsoft.com, если вы хотите, чтобы она также отслеживалась там.


Минимальное воспроизведение:

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

Код, сгенерированный в минимальном репро, эквивалентен

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

куда infoof это поддельный оператор, который получает MethodInfo для данного метода.

Правильный код будет:

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

Equal Метод не может иметь дело с одним обнуляемым, одним ненулевым операндом. Это требует, чтобы либо оба обнулялись, либо нет.

(Обратите внимание, что false верно. Этот логический тип определяет, является ли результат снятого равенства логическим значением; в C# это не так, в VB это.)

Да, эта ошибка исчезла в Roslyn (компилятор в разработке). Посмотрим на существующий продукт.

Другие вопросы по тегам