2016-08-31 38 views
9

Mam pewien kod, który odwzorowuje silnie typowane obiekty biznesowe na typy anonimowe, które następnie są serializowane do JSON i odsłonięte za pośrednictwem interfejsu API.Dlaczego Object.Equals() zwraca false dla identycznych typów anonimowych, gdy są tworzone z różnych zestawów?

Po restrukturyzacji mojego rozwiązania na osobne projekty, niektóre z moich testów zaczęły zawieść. Zrobiłem trochę kopania i okazuje się, że Object.Equals zachowuje się inaczej na anonimowych typach, które są zwracane przez kod z innego zespołu - i nie jestem pewien, dlaczego lub co mogę zrobić, aby obejść to.

Istnieje pełny kod repro w https://github.com/dylanbeattie/AnonymousTypeEquality, ale bit, który faktycznie łamie się, znajduje się poniżej. Ten kod jest w projekcie badania:

[TestFixture] 
public class Tests { 
    [Test] 
    public void BothInline() { 
     var a = new { name = "test", value = 123 }; 
     var b = new { name = "test", value = 123 }; 
     Assert.That(Object.Equals(a,b)); // passes 
    } 

    [Test] 
    public void FromLocalMethod() { 
     var a = new { name = "test", value = 123 }; 
     var b = MakeObject("test", 123); 
     Assert.That(Object.Equals(a, b)); // passes 
    } 

    [Test] 
    public void FromOtherNamespace() { 
     var a = new { name = "test", value = 123 }; 
     var b = OtherNamespaceClass.MakeObject("test", 123); 
     Assert.That(Object.Equals(a, b)); // passes 
    } 


    [Test] 
    public void FromOtherClass() { 
     var a = new { name = "test", value = 123 }; 
     var b = OtherClass.MakeObject("test", 123); 

     /* This is the test that fails, and I cannot work out why */ 
     Assert.That(Object.Equals(a, b)); 
    } 

    private object MakeObject(string name, int value) { 
     return new { name, value }; 
    } 
} 

a następnie istnieje odrębna biblioteka klasy w roztworze zawierającym tylko to:

namespace OtherClasses { 
    public static class OtherClass { 
    public static object MakeObject(string name, int value) { 
     return new { name, value }; 
    } 
    } 
} 

Według MSDN „dwie instancje samego typu anonimowego są równe, jeśli wszystkie ich właściwości są równe. " (mój nacisk) - więc co kontroluje, czy dwie instancje mają ten sam anonimowy typ dla celów porównawczych? Moje dwa wystąpienia mają jednakowe kody skrótu, a oba wydają się być <>f__AnonymousType0`2[System.String,System.Int32] - ale domyślam się, że równość dla typów anonimowych musi uwzględniać w pełni kwalifikowaną nazwę typu, a zatem przeniesienie kodu do innego zespołu może spowodować błędy. Ktoś dostał ostateczne źródło/link dokładnie o tym, jak jest to realizowane?

Odpowiedz

4

Jeśli rozbierać swoje zespoły za pomocą narzędzia podobnie jak Reflector, zobaczysz, że twój anonimowy typ jest reprezentowany przez klasę w każdym zestawie, który wygląda tak (po rozplątaniu generowanych przez kompilator identyfikatorów):

internal sealed class AnonymousType<TName, TValue> 
{ 
    private readonly TName _name; 
    private readonly TValue _value; 

    public TName name => this._name; 
    public TValue value => this._value; 

    public AnonymousType(TName name, TValue value) 
    { 
     this._name = name; 
     this._value = value; 
    } 

    public override bool Equals(object value) 
    { 
     var that = value as AnonymousType<TName, TValue>; 
     return that != null && 
      EqualityComparer<TName>.Default.Equals(this._name, that._name) && 
      EqualityComparer<TValue>.Default.Equals(this._value, that._value); 
    } 

    public override int GetHashCode() 
    { 
     // ... 
    } 
} 

Pierwsza linia metody Equals sprawdza, czy value jest instancją AnonymousType<TName, TValue>, odnosząc się konkretnie do klasy zdefiniowanej jako w bieżącym zestawie.Dlatego anonimowe typy z różnych zespołów nigdy nie będą porównywać równych, nawet jeśli mają taką samą strukturę.

Może zajść potrzeba zmiany testów w celu porównania seryjnego JSON obiektów, a nie samych obiektów.

8

Typy anonimowe są z natury zakresowe. Twój przykład przerywa to ustalanie zakresu, więc typy są różne. W obecnych kompilatorach C# typy anonimowe nie mogą przekraczać złożeń (lub modułów, aby być dokładniejszymi). Nawet jeśli dwa anonimowe typy z dwóch różnych zestawów mają te same właściwości, są to dwa różne typy (i są to internal, więc uważaj na implikacje związane z bezpieczeństwem). Po drugie, spychasz anonimowy typ na object, ty wiesz, robisz to źle.

TL; DR: Nadużywasz anonimowych typów. Nie zdziw się, że Cię gryzie.

+0

Nie mam problemu z ukąszeniem raz na jakiś czas ... dobrze jest wiedzieć, co dokładnie zrobiłeś, aby sprowokować ukąszenie. :) –

+1

@DylanBeattie Rzuciłeś anonimowy typ na 'object'. Nigdy tego nie rób. Zachowaj typ ogólny (tak jak robi LINQ) lub upewnij się, że jest lokalny z poprawnym typem. Wszystko inne to prohibidado. – Luaan

2

Typy anonimowe zostają skompilowane do ukrytego typu wewnątrz zespołu, w którym się znajdują, który jest ponownie wykorzystywany dla celów efektywności, jeśli definicja pasuje. Oznacza to, że podobne AT w różnych złożeniach będą różnych typów, a ich .Equals wykona kontrolę typu.

Oto jeden z moje ulubione rzeczy do zrobienia z typów Anon:

void Main() 
{ 
    var json = "{ \"name\": \"Dylan\"}"; 
    var x = Deserialize(json, new { name = null as string}); 
    Console.WriteLine(x.name); 
} 

T Deserialize<T>(string json, T template) 
{ 
    return (T) JsonConvert.DeserializeObject(json, typeof(T)); 
} 

Byłoby interesujące umieścić metodę deserializowania w innym zespole ...