Равенство в деревьях выражений не использует правильную перегрузку оператора

#c# #expression-trees #roslyn

#c# #деревья выражений #рослин

Вопрос:

У меня возникла странная проблема с деревьями выражений и перегрузкой операторов (в частности, с == != операторами and ).

Я использую MemberwiseComparer из одного из ответов Марка Гравелла, более или менее одного

 public static class MemberComparer
{
    public static bool Equal<T>(T x, T y)
    {
        return EqualComparerCache<T>.Compare(x, y);
    }

    static class EqualComparerCache<T>
    {
        internal static readonly Func<T, T, bool> Compare = (a, b) => true;

        static EqualComparerCache()
        {
            var members = typeof(T).GetTypeInfo().DeclaredProperties.Cast<MemberInfo>()
                .Concat(typeof(T).GetTypeInfo().DeclaredFields.Where(p => !p.IsStatic amp;amp; p.IsPublic).Cast<MemberInfo>());
            var x = Expression.Parameter(typeof(T), "x");
            var y = Expression.Parameter(typeof(T), "y");

            Expression body = null;
            foreach (var member in members)
            {
                Expression memberEqual;
                if (member is FieldInfo)
                {
                    memberEqual = Expression.Equal(
                        Expression.Field(x, (FieldInfo)member),
                        Expression.Field(y, (FieldInfo)member));
                }
                else if (member is PropertyInfo)
                {
                    memberEqual = Expression.Equal(
                        Expression.Property(x, (PropertyInfo)member),
                        Expression.Property(y, (PropertyInfo)member));
                }
                else
                {
                    throw new NotSupportedException(member.GetType().GetTypeInfo().Name);
                }

                body = body == null ? memberEqual : Expression.AndAlso(body, memberEqual);
            }

            if (body != null)
            {
                var lambda = Expression.Lambda<Func<T, T, bool>>(body, x, y);
                Compare = lambda.Compile();
            }
        }
    }
}
  

И базовый класс ValueObject<T> , который служит базовым классом для объектов значений.

 public class ValueObject<T> : IEquatable<T> where T : ValueObject<T>
{
    public virtual bool Equals(T other)
    {
        if (ReferenceEquals(this, other))
            return true;

        return MemberComparer.Equal<T>((T)this, other);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as T);
    }

    public override int GetHashCode()
    {
        return MemberComparer.GetHashCode((T)this);
    }

    public static bool operator ==(ValueObject<T> left, ValueObject<T> right)
    {
        // If both are null, or both are same instance, return true.
        if (ReferenceEquals(left, right))
        {
            return true;
        }

        // If one is null, but not both, return false.
        if (((object)left == null) || ((object)right == null))
        {
            return false;
        }

        return left.Equals(right);
    }

    public static bool operator !=(ValueObject<T> left, ValueObject<T> right)
    {
        return !(left == right);
    }
}
  

В целом это отлично работает для классов, которые реализуют IEquatable<T> или скалярные типы и / или строки. Однако, когда класс содержит свойства, которые реализуют классы ValueObject<T> , сравнение завершается ошибкой.

 public class Test : ValueObject<Test>
{
    public string Value { get; set; }
}

public class Test2 : ValueObject<Test2>
{
    public Test Test { get; set; }
}
  

При сравнении Test с Test ним работает нормально.

 var test1 = new Test { Value = "TestValue"; }
var test2 = new Test { Value = "TestValue"; }

Assert.True(test1==test2); // true
Assert.Equals(test1, test2); // true
  

Но при сравнении Test2 происходит сбой:

 var nestedTest1 = new Test2 { Test = new Test { Value = "TestValue"; } }
var nestedTest2 = new Test2 { Test = new Test { Value = "TestValue"; } }

Assert.True(nestedTest1==nestedTest2 ); // false
Assert.Equals(nestedTest1, nestedTest2 ); // false

// Second Test with referenced Test object
var test = new Test { Value = "TestValue"; }
var nestedTest1 = new Test2 { Test = test }
var nestedTest2 = new Test2 { Test = test }

Assert.True(nestedTest1==nestedTest2 ); // true
Assert.Equals(nestedTest1, nestedTest2 ); // true
  

Переопределение == оператора вызывается для Test2 класса, но не для Test класса. Когда nestedTest1 и nestedTest2 ссылаются на один и тот же Test объект, это работает. Таким == образом, перегрузка не вызывается при построении и компиляции выражения.

Я не мог найти причину, по которой он будет игнорировать это. Это какое-то изменение в Roslyn, которое никто не заметил, или что-то не так с генерацией дерева выражений?

Конечно, я мог бы переписать генерацию дерева выражений для вызова .Equals метода вместо этого, но это добавило бы больше сложности (и дополнительных проверок null). Но на самом деле вопрос в том, почему скомпилированное дерево выражений не использует == перегрузку и как заставить ее работать?

Ответ №1:

Немного покопавшись, вот в чем проблема. Оператор == не определен в классе Test , но он определен в ValueType<T> .

Если вы вызываете,

 // this is used by Expression.Equal (it does not search for base type)

var m = typeof(Test).GetMethod("op_Equality", 
            BindingFlags.Static 
            | BindingFlags.Public | BindingFlags.NonPublic);

//m is null because op_Equality is not declared on "Test"

var m = typeof(ValueObject<>).GetMethod("op_Equality", 
            BindingFlags.Static 
            | BindingFlags.Public | BindingFlags.NonPublic);

// m is not null
  

По этой причине выражение не использует метод равенства операторов.

Кажется, Roslyn использует оператор равенства при его компиляции, но компилятор выражений не является частью Roslyn, и это кажется ошибкой в строке http://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Ast/BinaryExpression.cs ,b3df2869d7601af4, где он не выполняет поиск метода в базовых классах.

Комментарии:

1. Сначала я тоже так подумал, но я не понимаю, почему Assert.True(test1==test2); это сработало, но то же выражение ( memberEqual = Expression.Equal(Expression.Property(x, (PropertyInfo)member),Expression.Property(y, (PropertyInfo)member)); ) через деревья выражений не работает. В приведенном выше случае оператор == вызывается, когда я устанавливаю в нем точки останова.

2. Потому что оператор для строки определен в классе string . Также существует разница между компилятором c # и компилятором выражений. оператор == вызывается компилятором c #. Может быть, это ошибка.

Ответ №2:

В итоге я реализую метод, который ищет op_Equality метод переопределения оператора и передает его в Expression.Equal качестве 4-го параметра.

 MethodInfo equalsOperator = FindMethod(memberType, "op_Equality", false);

equalityExpression = Expression.Equal(
    Expression.Property(left, memberInfo),
    Expression.Property(right, memberInfo),
    false,
    equalsOperator);

... 
private static MethodInfo FindMethod(Type type, string methodName, bool throwIfNotFound = true)
{
    TypeInfo typeInfo = type.GetTypeInfo();

    // TODO: Improve to search methods with a specific signature and parameters
    while (typeInfo != null)
    {
        IEnumerable<MethodInfo> methodInfo = typeInfo.GetDeclaredMethods(methodName);
        if (methodInfo.Any())
            return methodInfo.First();

        typeInfo = typeInfo.BaseType?.GetTypeInfo();
    }

    if (!throwIfNotFound)
        return null;

    throw new InvalidOperationException($"Type '{type.GetTypeInfo().FullName}' has no '{methodName}' method.");
}
  

В моем простом сценарии достаточно (на данный момент) использовать первое op_Equality найденное, в классе должно быть не более одного ValueObject<T> , и я убедился MemberComparer.Equal<T>((T)this, other) , что вызывается только тогда, когда оба объекта имеют один и тот же тип.