2016-09-29 1 views
0

式ツリーと演算子のオーバーロード(特に==!=演算子)で何か問題があります。式ツリーの等価性が正しい演算子オーバーロードを使用していません

私はマルクGravellの答えの一つ、値オブジェクトの基本クラスとして機能します多かれ少なかれ1

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<T>からMemberwiseComparerを使用しています。

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

それが正常に動作します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 

==演算子のオーバーライドはなくTestクラスのため、Test2クラスのために呼ばれています。 nestedTest1nestedTest2が同じTestオブジェクトを参照すると動作します。したがって、式が構築され、コンパイルされるときに==オーバーロードは呼び出されません。

私はそれを無視する理由を見つけることができませんでした。これはRoslynに何らかの変更がありますか?気づいた人はいませんか、または表現木の生成に何か問題がありますか?

もちろん、.Equalsメソッドを呼び出すように式ツリーの生成を書き直すことはできますが、これにより複雑さ(および追加のヌルチェック)が追加されます。しかし実際の質問は、なぜコンパイルされた式ツリーで==の過負荷が使用されていないのですか?

答えて

0

最後に、op_Equality演算子オーバーライドメソッドを検索し、それを第4パラメータとしてExpression.Equalに渡す方法を実装します。それは最初op_Equalityが見つかっ使用する(今のところ)で十分だ私の単純なシナリオでは

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

ValueObject<T>クラスの1以下であるはずですし、両方のオブジェクトがでているとき、私は、MemberComparer.Equal<T>((T)this, other)だけと呼ばれていることを確認しました同じタイプ。

2

少し掘り下げた後、ここに問題があります。演算子==はクラス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 

これは、Expressionがオペレータ平等メソッドを使用していない理由です。

Roslynはコンパイル時に等価演算子を使用しますが、ExpressionコンパイラはRoslynの一部ではないようですが、これは基本クラスのメソッドを検索しない行http://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Ast/BinaryExpression.cs,b3df2869d7601af4のバグです。

+0

私はそれも考えましたが、なぜ 'Assert.True(test1 == test2); 'はうまくいったのですが、同じ式(' memberEqual = Expression.Equal(Expression.Property(x 、(PropertyInfo)メンバ)、Expression.Property(y、(PropertyInfo)メンバ));)上記の場合、ブレークポイントを設定すると、==演算子が呼び出されます。 – Tseng

+0

Stringの演算子は文字列クラスで定義されているためです。また、C#コンパイラと式コンパイラには違いがあります。 ==演算子は、C#コンパイラによって呼び出されます。それはバグかもしれない。 –

関連する問題