表达式树中的相等性没有使用正确的运算符重载
Equality in Expression Trees doesn't use the correct operator overload
我在表达式树和运算符重载方面遇到了一些奇怪的问题(特别是 ==
和 !=
运算符)。
我正在使用 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();
}
}
}
}
还有一个基 class ValueObject<T>
作为值对象的基 class。
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>
或标量类型 and/or 字符串的 classes。但是,当 class 包含实现 ValueObject<T>
的 class 属性时,比较失败。
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
class 调用了 ==
运算符覆盖,但没有为 Test
class 调用。当 nestedTest1
和 nestedTest2
引用相同的 Test
对象时,它起作用。因此在构建和编译表达式时不会调用 ==
重载。
我找不到它会忽略它的原因。这是对 Roslyn 的一些更改没有人注意到,还是表达式树生成有问题?
当然我可以重写表达式树生成来调用 .Equals
方法,但这会增加更多的复杂性(和额外的空值检查)。但实际问题是,为什么编译后的表达式树不使用 ==
重载以及如何使其工作?
深入挖掘后,问题来了。运算符 ==
未在 class 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 中的错误,它不在基 [=23] 中搜索方法=]es.
我最终实现了一种搜索 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<T>
class 中不应超过一个,我确保 MemberComparer.Equal<T>((T)this, other)
仅在两个对象属于同一类型时调用。
我在表达式树和运算符重载方面遇到了一些奇怪的问题(特别是 ==
和 !=
运算符)。
我正在使用 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();
}
}
}
}
还有一个基 class ValueObject<T>
作为值对象的基 class。
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>
或标量类型 and/or 字符串的 classes。但是,当 class 包含实现 ValueObject<T>
的 class 属性时,比较失败。
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
class 调用了 ==
运算符覆盖,但没有为 Test
class 调用。当 nestedTest1
和 nestedTest2
引用相同的 Test
对象时,它起作用。因此在构建和编译表达式时不会调用 ==
重载。
我找不到它会忽略它的原因。这是对 Roslyn 的一些更改没有人注意到,还是表达式树生成有问题?
当然我可以重写表达式树生成来调用 .Equals
方法,但这会增加更多的复杂性(和额外的空值检查)。但实际问题是,为什么编译后的表达式树不使用 ==
重载以及如何使其工作?
深入挖掘后,问题来了。运算符 ==
未在 class 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 中的错误,它不在基 [=23] 中搜索方法=]es.
我最终实现了一种搜索 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<T>
class 中不应超过一个,我确保 MemberComparer.Equal<T>((T)this, other)
仅在两个对象属于同一类型时调用。