2013-05-13 17 views
9

otrzymuje dwie identyczne anonimowe obiekty typu:Czy można bezpiecznie używać GetHashCode do porównywania identycznych typów anonimowych?

{msg:"hello"} //anonType1 
{msg:"hello"} //anonType2 

i zakładamy, że nie zostały one rozwiązane do tego samego typu (np mogą być definiowane w różnych zespołach)

anonType1.Equals(anonType2); //false 

Ponadto zakładamy, że w czas kompilacji, nie mogę uzyskać struktury jednego (np. anonType1), ponieważ interfejs API tylko eksponuje object

Więc, aby je porównać, pomyślałem o następujących technikach:

  1. Użyj odbicia, aby uzyskać porównanie z wartością msg na anonType1.
  2. Obsada anonType1 do dynamic rodzaju i odniesienie .msg na dynamicznym członka do porównania
  3. Porównaj wynik .GetHashCode() na każdym obiekcie.

Moje pytanie brzmi: czy można bezpiecznie używać opcji 3? To znaczy. czy rozsądnie jest założyć, że implementacja .GetHashcode() zawsze zwróci tę samą wartość dla indentycznie ustrukturyzowanych, ale różnych typów anonimowych w obecnej i wszystkich przyszłych wersjach platformy .NET?

+0

Uwaga: I dodaje 'Expression' opartego memberwise comparer - może być przydatna –

+0

Brilliant, dzięki. –

Odpowiedz

5

Interesujące pytanie. Specyfikacja określa, że ​​metody Equals i GetHashcode (zauważ literówkę w specyfikacji!) Będą zachowywać się dla instancji tego samego typu, jednak implementacja nie jest zdefiniowana. Tak się składa, że ​​obecny kompilator MS C# implementuje to za pomocą liczb magicznych, takich jak nasiona -1134271262 i mnożnik -1521134295. Ale ten nie jest częścią specyfikacji. Teoretycznie może to radykalnie zmienić się między wersjami kompilatora C# i nadal będzie spełniał to, czego potrzebuje. Jeśli więc 2 zestawy nie są kompilowane przez ten sam kompilator, nie ma gwarancji. Rzeczywiście, byłoby "ważne" (ale mało prawdopodobne), aby kompilator wymyślił nową wartość początkową za każdym razem, gdy się kompiluje.

Osobiście przyjrzałbym się technikom IL lub Expression, aby to zrobić. Porównywanie obiektów o podobnym kształcie z podziałem na nazwy jest dość łatwe w wykonaniu z Expression.

Dla informacji, ja również spojrzał na jak mcs (kompilator Mono) realizuje GetHashCode i jest inaczej; zamiast nasion i mnożników używa kombinacji nasion, xor, mnożnika, przesunięć i dodatków. Tak więc ten sam typ kompilowany przez Microsoft i Mono będzie miał bardzo inny GetHashCode.

static class Program { 
    static void Main() { 
     var obj = new { A = "abc", B = 123 }; 
     System.Console.WriteLine(obj.GetHashCode()); 
    } 
} 
  • Mono: -2077468848
  • Microsoft: -617335881

Zasadniczo, nie sądzę, można tego zagwarantować.


Jak o:

using System; 
using System.Linq; 
using System.Linq.Expressions; 
using System.Reflection; 
class Foo 
{ 
    public string A { get; set; } 
    public int B; // note a field! 
    static void Main() 
    { 
     var obj1 = new { A = "abc", B = 123 }; 
     var obj2 = new Foo { A = "abc", B = 123 }; 
     Console.WriteLine(MemberwiseComparer.AreEquivalent(obj1, obj2)); // True 

     obj1 = new { A = "abc", B = 123 }; 
     obj2 = new Foo { A = "abc", B = 456 }; 
     Console.WriteLine(MemberwiseComparer.AreEquivalent(obj1, obj2)); // False 

     obj1 = new { A = "def", B = 123 }; 
     obj2 = new Foo { A = "abc", B = 456 }; 
     Console.WriteLine(MemberwiseComparer.AreEquivalent(obj1, obj2)); // False 
    } 

} 

public static class MemberwiseComparer 
{ 
    public static bool AreEquivalent(object x, object y) 
    { 
     // deal with nulls... 
     if (x == null) return y == null; 
     if (y == null) return false; 
     return AreEquivalentImpl((dynamic)x, (dynamic)y); 
    } 
    private static bool AreEquivalentImpl<TX, TY>(TX x, TY y) 
    { 
     return AreEquivalentCache<TX, TY>.Eval(x, y); 
    } 
    static class AreEquivalentCache<TX, TY> 
    { 
     static AreEquivalentCache() 
     { 
      const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; 
      var xMembers = typeof(TX).GetProperties(flags).Select(p => p.Name) 
       .Concat(typeof(TX).GetFields(flags).Select(f => f.Name)); 
      var yMembers = typeof(TY).GetProperties(flags).Select(p => p.Name) 
       .Concat(typeof(TY).GetFields(flags).Select(f => f.Name)); 
      var members = xMembers.Intersect(yMembers); 

      Expression body = null; 
      ParameterExpression x = Expression.Parameter(typeof(TX), "x"), 
           y = Expression.Parameter(typeof(TY), "y"); 
      foreach (var member in members) 
      { 
       var thisTest = Expression.Equal(
        Expression.PropertyOrField(x, member), 
        Expression.PropertyOrField(y, member)); 
       body = body == null ? thisTest 
        : Expression.AndAlso(body, thisTest); 
      } 
      if (body == null) body = Expression.Constant(true); 
      func = Expression.Lambda<Func<TX, TY, bool>>(body, x, y).Compile(); 
     } 
     private static readonly Func<TX, TY, bool> func; 
     public static bool Eval(TX x, TY y) 
     { 
      return func(x, y); 
     } 
    } 
} 
+0

Jeśli istniała specyfikacja, która definiowała dokładnie anonimowe klasy, które kompilatory muszą tworzyć, wówczas interoperacyjność byłaby banalna. Bez specyfikacji tego, jak powinny wyglądać klasy generowane przez kompilator, nie widzę możliwości, aby takie klasy były interoperacyjne, nawet gdyby chciały być. Fakt, że niektóre dowolne klasy mają właściwości, których nazwy i wartości odpowiadają klasom anonimowym, nie oznacza, że ​​przykład tego pierwszego powinien być równy przypadkowi tego ostatniego. – supercat

+0

@supercat z drugiej strony, jest mało prawdopodobne, aby porównywać 2 obiekty, jeśli ich nie jest * semantyczna * pseudo-równoważność między nimi –

+0

Dla wszystkich nie-zerowych typów "X" i "Y" z nieprzeciętymi przesłonięciami 'Equals (obiekt)', '((obiekt) X) .Equals (Y)' i '((obiekt) Y) .Equals (X)' powinien zawsze zwracać tę samą wartość, bez wyjątków. Mając typ raportu, jego wystąpienia jako równe rzeczom jakiegoś niepowiązanego typu, który nic o nim nie wie, może spowodować, że zbiory takie jak 'Dictionary > będą działać nieprawidłowo, jeśli obiekty obu typów są w nim przechowywane. – supercat