10

Given two identical anonymous type objects:

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

And assume that they haven't resolved to the same type (e.g. they might be defined in different assemblies)

anonType1.Equals(anonType2); //false

Furthermore, assume that at compile time, I can't get the structure of one (say anonType1) because the API only exposes object

So, to compare them, I thought of the following techniques:

  1. Use reflection to get the msg property on anonType1 for comparison.
  2. Cast anonType1 to a dynamic type and reference .msg on the dynamic member for comparison
  3. Compare the result of .GetHashCode() on each object.

My question is: Is it safe to use Option 3? I.e. is it sensible to assume that the .GetHashcode() implementation will always return the same value for indentically-structured, but different anonymous types in the current and all future versions of the .NET framework?

James Wiseman
  • 29,946
  • 17
  • 95
  • 158

1 Answers1

6

Interesting question. The specification defines that Equals and GetHashcode (note the typo in the specification!) methods will behave for instances of the same type, however the implementation is not defined. As it happens, the current MS C# compiler implements this using magic numbers like a seed of -1134271262 and a multiplier of -1521134295. But that is not part of the specification. Theoretically that could change radically between C# compiler versions and it would still meet what it needs to. So if the 2 assemblies are not compiled by the same compiler, there is no guarantee. Indeed, it would be "valid" (but unlikely) for the compiler to think up a new seed value every time it compiles.

Personally, I would look at using IL or Expression techniques to do this. Comparing similarly-shaped objects member-wise by name is fairly easy to do with Expression.

For info, I've also looked at how mcs (the Mono compiler) implements GetHashCode, and it is different; instead of seed and multiplier, it uses a combination of seed, xor, multiplier, shifts and additions. So the same type compiled by Microsoft and Mono will have very different GetHashCode.

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

Basically, I do not think you can guarantee this.


How about:

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);
        }
    }
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • If there were a spec that defined precisely the anonymous classes that compilers must create, then interoperability would be trivial. Without a spec for what the compiler-generated classes should look like, I see no way such classes could be interoperable even if they wanted to be. That fact that some arbitrary class has properties whose names and values match those of an anonymous class doesn't mean an instance of the former should compare equal to an instance of the latter. – supercat Jun 03 '13 at 19:43
  • @supercat on the other hand, you are unlikely to be comparing 2 objects if their isn't a *semantic* pseudo-equivalence between them – Marc Gravell Jun 03 '13 at 19:54
  • For all non-null `X` and `Y` of types with non-broken overrides of `Equals(object)`, `((object)X).Equals(Y)` and `((object)Y).Equals(X)` should always return the same value, with no exceptions. Having a type report its instances as equal to things of some unrelated type that knows nothing about it is apt to cause collections such as `Dictionary` to malfunction if objects of both types are stored therein. – supercat Jun 03 '13 at 20:13
  • @supercat which is why dictionaries allow an external comparer object to be passed in, which can own the comparison rules – Marc Gravell Jun 04 '13 at 06:48
  • Certainly dictionaries can override the comparison rules (generally a good thing, though it can make serialization difficult) but it is expected that every objects' "default" equality comparator (i.e. its overrides of `Equals` and `GetHashCode`) should behave consistently; there would be nothing wrong with having an `ObjectPropertyComparer` which regarded as equal things whose properties returned equal values, but objects should not override their own equality method that way unless they can determine whether any object they're compared against will do likewise. BTW, ... – supercat Jun 04 '13 at 14:51
  • ...a custom equality comparer would have the advantage that it would be better placed than an `Object.Equals` method to build a "two-dimensional" dictionary of comparison methods so that Reflection would only have to be used once for each pair of types to be compared. – supercat Jun 04 '13 at 14:55