Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeDelegator equality inconsistency?

Consider the following code:

    class MyType : TypeDelegator
    {
       public MyType(Type parent)
          : base(parent)
       {
       }
    }

    class Program
    {
       static void Main(string[] args)
       {
          Type t1 = typeof(string);
          Type t2 = new MyType(typeof(string));

          Console.WriteLine(EqualityComparer<Type>.Default.Equals(t1, t2)); // <-- false
          Console.WriteLine(EqualityComparer<Type>.Default.Equals(t2, t1)); // <-- true

          Console.WriteLine(t1.Equals(t2)); // <-- true
          Console.WriteLine(t2.Equals(t1)); // <-- true

          Console.WriteLine(Object.Equals(t1, t2)); // <-- false
          Console.WriteLine(Object.Equals(t2, t1)); // <-- true
       }
   }

How come the various versions of Equals return different results? The EqualityComparer.Default probably calls Object.Equals, so these results match, although inconsistent in themselves. And the normal instance version of Equals both return true.

This obviously creates problems when having a method return a Type that actually inherits from TypeDelegator. Imagine for example placing these types as keys in a dictionary, which by default use the EqualityComparer.Default for comparisons.

Is there any way to resolve this problem? I would like all the methods in the code above return true.

like image 317
DeCaf Avatar asked Oct 03 '11 09:10

DeCaf


3 Answers

The following code returns a System.RuntimeType

Type t1 = typeof(string);

If you look at the code for Type there is:

public override bool Equals(Object o)
{
    if (o == null) 
        return false;

    return Equals(o as Type); 
}

BUT, System.RuntimeType has:

public override bool Equals(object obj) 
{
    // ComObjects are identified by the instance of the Type object and not the TypeHandle.
    return obj == (object)this;
} 

And if you view the assembly it executes a: cmp rdx, rcx, so just a direct memory compare.

You can reproduce it using the following:

bool a = t1.Equals((object)t2); // False
bool b = t1.Equals(t2); // True

So it looks like RuntimeType is overriding the Type Equals method to do a direct comparison... It would appear there is no easy way around the issue (without supplying a comparer).

EDITED TO ADD: Out of curiosity, I had a look at the .NET 1.0 & 1.1 implementation of RuntimeType. They don't have the override of Equals in RuntimeType, so the issue was introduced in .NET 2.0.

like image 63
Steven Avatar answered Nov 10 '22 14:11

Steven


Update

The code from this answer has become a repository on GitHub: Undefault.NET on GitHub

Steven gives a good explanation of why this works the way it does. I do not believe there is a solution for the Object.Equals case. However,

I've found a way to fix the issue in the EqualityComparer<T>.Default case by configuring the default equality comparer with reflection.

This little hack only needs to happen once per application life cycle. Startup would be a good time to do this. The line of code that will makes it work is:

DefaultComparisonConfigurator.ConfigureEqualityComparer<Type>(new HackedTypeEqualityComparer());

After that code has been executed, EqualityComparer<Type>.Default.Equals(t2, t1)) will yield the same result as EqualityComparer<Type>.Default.Equals(t1,t2)) (in your example).

The supporting infrastructure code includes:

1. a custom IEqualityComparer<Type> implementation

This class handles equality comparison the way that you want it to behave.

public class HackedTypeEqualityComparer : EqualityComparer<Type> { 

    public override bool Equals(Type one, Type other){
        return ReferenceEquals(one,null) 
            ? ReferenceEquals(other,null)
            : !ReferenceEquals(other,null) 
                && ( (one is TypeDelegator || !(other is TypeDelegator)) 
                    ? one.Equals(other) 
                    : other.Equals(one));
    }

    public override int GetHashCode(Type type){ return type.GetHashCode(); }

}

2. a Configurator class

This class uses reflection to configure the underlying field for EqualityComparer<T>.Default. As a bonus, this class exposes a mechanism to manipulate the value of Comparer<T>.Default as well, and ensures that the results of configured implementations are compatible. There is also a method to revert configurations back to the Framework defaults.

public class DefaultComparisonConfigurator
{ 

    static DefaultComparisonConfigurator(){
        Gate = new object();
        ConfiguredEqualityComparerTypes = new HashSet<Type>();
    }

    private static readonly object Gate;
    private static readonly ISet<Type> ConfiguredEqualityComparerTypes;

    public static void ConfigureEqualityComparer<T>(IEqualityComparer<T> equalityComparer){ 
        if(equalityComparer == null) throw new ArgumentNullException("equalityComparer");
        if(EqualityComparer<T>.Default == equalityComparer) return;
        lock(Gate){
            ConfiguredEqualityComparerTypes.Add(typeof(T));
            FieldFor<T>.EqualityComparer.SetValue(null,equalityComparer);
            FieldFor<T>.Comparer.SetValue(null,new EqualityComparerCompatibleComparerDecorator<T>(Comparer<T>.Default,equalityComparer));
        }
    }

    public static void ConfigureComparer<T>(IComparer<T> comparer){
        if(comparer == null) throw new ArgumentNullException("comparer");
        if(Comparer<T>.Default == comparer) return;
        lock(Gate){
            if(ConfiguredEqualityComparerTypes.Contains(typeof(T)))
                FieldFor<T>.Comparer.SetValue(null,new EqualityComparerCompatibleComparerDecorator<T>(comparer,EqualityComparer<T>.Default));
            else 
                FieldFor<T>.Comparer.SetValue(null,comparer);
        }
    }

    public static void RevertConfigurationFor<T>(){
        lock(Gate){
            FieldFor<T>.EqualityComparer.SetValue(null,null);
            FieldFor<T>.Comparer.SetValue(null,null);
            ConfiguredEqualityComparerTypes.Remove(typeof(T));
        }   
    }

    private static class FieldFor<T> { 

        private const string FieldName = "defaultComparer";
        private const BindingFlags FieldBindingFlags = BindingFlags.NonPublic|BindingFlags.Static;

        static FieldInfo comparer, equalityComparer;

        public static FieldInfo Comparer { get { return comparer ?? (comparer = typeof(Comparer<T>).GetField(FieldName,FieldBindingFlags)); } }

        public static FieldInfo EqualityComparer { get { return equalityComparer ?? (equalityComparer = typeof(EqualityComparer<T>).GetField(FieldName,FieldBindingFlags)); } }

    }
} 

3. a compatible IComparer<T> implementation

This is basically a decorator for IComparer<T> that ensures compatibility between Comparer<T> and EqualityComparer<T> when EqualityComparer<T> is injected. It makes sure that any two values that the configured IEqualityComparer<T> implementation thinks are equal will always have a comparison result of 0.

public class EqualityComparerCompatibleComparerDecorator<T> : Comparer<T> { 

    public EqualityComparerCompatibleComparerDecorator(IComparer<T> comparer, IEqualityComparer<T> equalityComparer){
        if(comparer == null) throw new ArgumentNullException("comparer");
        if(equalityComparer == null) throw new ArgumentNullException("equalityComparer");
        this.comparer = comparer;
        this.equalityComparer = equalityComparer;
    }

    private readonly IComparer<T> comparer;
    private readonly IEqualityComparer<T> equalityComparer;

    public override int Compare(T left, T right){ return this.equalityComparer.Equals(left,right) ?  0 : comparer.Compare(left,right); }

}
like image 4
smartcaveman Avatar answered Nov 10 '22 15:11

smartcaveman


Fascinating q.

The middle Equals both being true are because Type.Equals returns the value of ReferenceEquals as invoked on the UnderlyingSystemType property for both sides - and TypeDelegator overrides UnderlyingSystemType to return the Type you constructed it with!

How you can persuade a non-Type-ish equality operation to understand this, I don't know. I suspect you can't, and you'll need to always supplier a suitably aware EqualityComparer.

like image 2
AakashM Avatar answered Nov 10 '22 14:11

AakashM