【问题标题】:Equality in Expression Trees doesn't use the correct operator overload表达式树中的相等不使用正确的运算符重载
【发布时间】:2017-02-08 03:03:58
【问题描述】:

表达式树和运算符重载(尤其是 ==!= 运算符)遇到了一些奇怪的问题。

我正在使用 Marc Gravell 的答案之一中的 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 && 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&lt;T&gt;,用作值对象的基类。

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&lt;T&gt; 或标量类型和/或字符串的类。但是,当类包含实现ValueObject&lt;T&gt; 的类的属性时,比较失败。

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

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

当比较 TestTest 时,它工作正常。

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 类调用的。当nestedTest1nestedTest2 引用相同的Test 对象时,它可以工作。因此,在构建和编译表达式时,不会调用 == 重载。

我找不到它会忽略它的原因。这是对 Roslyn 的一些更改,没有人注意到,还是表达式树的生成有问题?

当然,我可以重写表达式树生成来调用.Equals 方法,但这会增加更多复杂性(以及额外的空检查)。但实际的问题是,为什么编译后的表达式树不使用== 重载以及如何使其工作?

【问题讨论】:

    标签: c# expression-trees roslyn


    【解决方案1】:

    在深入挖掘之后,问题就来了。运算符 == 未在类 Test 中定义,但在 ValueType&lt;T&gt; 中定义。

    如果你打电话,

    // 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
    

    这就是 Expression 不使用运算符相等方法的原因。

    似乎 Roslyn 在编译时确实使用了相等运算符,但表达式编译器不是 Roslyn 的一部分,这似乎是 http://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Ast/BinaryExpression.cs,b3df2869d7601af4 行中的错误,它不会在基类中搜索方法。

    【讨论】:

    • 起初我也这么认为,但我不明白为什么Assert.True(test1==test2); 确实有效,但通过表达式树的相同表达式 (memberEqual = Expression.Equal(Expression.Property(x, (PropertyInfo)member),Expression.Property(y, (PropertyInfo)member));) 却没有。在上述情况下,当我在其中设置断点时会调用 == 运算符。
    • 因为 String 的操作符是在字符串类中定义的。 c#编译器和表达式编译器之间也有区别。 == 运算符由 c# 编译器调用。可能是错误。
    【解决方案2】:

    我最终实现了一个搜索op_Equality 运算符覆盖方法并将其作为第四个参数传递给Expression.Equal 的方法。

    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&lt;T&gt; 类中不应超过一个,并且我确保仅在两个对象都被调用时才调用 MemberComparer.Equal&lt;T&gt;((T)this, other)同类型。

    【讨论】:

      猜你喜欢
      • 2021-06-04
      • 1970-01-01
      • 1970-01-01
      • 2011-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-29
      • 1970-01-01
      相关资源
      最近更新 更多