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
.
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.
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,
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:
IEqualityComparer<Type>
implementationThis 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(); }
}
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)); } }
}
}
IComparer<T>
implementationThis 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); }
}
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With