TypeDelegator相等性不一致?

22

考虑以下代码:

    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
       }
   }
为什么不同版本的 Equals 方法会返回不同的结果?EqualityComparer.Default 可能调用 Object.Equals,因此这些结果匹配,尽管本身不一致。普通实例版本的 Equals 方法都返回 true。
当方法返回继承自 TypeDelegator 的 Type 时,这显然会带来问题。例如,将这些类型作为字典中的键时,默认情况下会使用 EqualityComparer.Default 进行比较。
有没有办法解决这个问题?我希望上面的所有方法都返回 true。
4个回答

7
下面的代码返回一个System.RuntimeType。
Type t1 = typeof(string);

如果您查看Type的代码,则会发现以下内容:
public override bool Equals(Object o)
{
    if (o == null) 
        return false;

    return Equals(o as Type); 
}

但是,System.RuntimeType具有以下特点:
public override bool Equals(object obj) 
{
    // ComObjects are identified by the instance of the Type object and not the TypeHandle.
    return obj == (object)this;
} 

如果您查看汇编代码,它执行了一个比较操作:cmp rdx, rcx,因此只是直接的内存比较。

您可以使用以下方法进行复现:

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

看起来 RuntimeType 覆盖了 Type 的 Equals 方法以进行直接比较... 似乎没有简单的方法可以解决这个问题(除非提供一个比较器)。

编辑后添加: 出于好奇,我查看了 .NET 1.0 和 1.1 实现的 RuntimeType。它们在 RuntimeType 中没有覆盖 Equals,因此该问题是在 .NET 2.0 中引入的。


感谢您的分析。在将其转换为对象时,我并没有意识到这个问题。如果您问我,框架中的类型和相等性方面似乎存在一些不太一致的地方。 - DeCaf
我同意这不太对,看起来像是一个错误/意外的行为。 - Steven

4

更新

这个答案的代码已经成为GitHub上的一个存储库:Undefault.NET on GitHub

Steven给出了关于为什么会按照这种方式工作的很好的解释。我不认为有解决Object.Equals情况的方法。但是,

我发现了一种通过使用反射配置默认相等比较器来解决EqualityComparer<T>.Default问题的方法。

这个小技巧只需要在应用程序生命周期内执行一次。启动时执行这个操作是个好时机。使它工作的代码行是:

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

执行该代码后,EqualityComparer<Type>.Default.Equals(t2, t1))将产生与EqualityComparer<Type>.Default.Equals(t1,t2))相同的结果(在您的示例中)。
支持基础设施代码包括:

1. 自定义IEqualityComparer<Type>实现

此类按您希望其行为处理等式比较。
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. 一个Configurator

这个类使用反射来配置EqualityComparer<T>.Default的基础字段。此外,该类还公开了一种机制来操纵Comparer<T>.Default的值,并确保配置实现的结果是兼容的。还有一种方法可以将配置恢复到框架默认值。

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. 兼容的IComparer<T>实现

这基本上是IComparer<T>的装饰器,确保在注入EqualityComparer<T>时,Comparer<T>EqualityComparer<T>之间的兼容性。它确保配置的IEqualityComparer<T>实现认为相等的任何两个值始终具有比较结果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); }

}

2

EqualityComparer<T>默认使用object.Equals方法,因此1)和2)的情况等同于5)和6)。

我不明白为什么这些比较默认应该是一致的。真正的情况发生在System.Type相等实现基于UnderlyingSystemType属性。因此,您可以重写Equals(object)Equals(Type) - 顺便说一下,在Framework 4上只有虚拟的 - 但这并不能修复第3种情况。

因此,要确保它是一致的,您可以这样做:

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

        public override Type UnderlyingSystemType
        {
            get
            {
                return this;
            }
        }
    }

通过这种实现方式,所有情况都会报告false,这是一致的,但我对副作用不确定...我想这取决于您的代码最终执行了什么。


2
覆盖 UnderlyingSystemType 以返回 CLR 提供的运行时类型之外的任何内容似乎是一个坏主意,并且可能会破坏一些代码。当然,我认为它应该是一致的,因为不一致会令人困惑,而根据参数顺序返回不同结果的 Equals 就是错误的。我开始怀疑没有好的解决方案,因为 System.Type 没有实现 IEquatable。(为什么没有呢?)如果它实现了,Object.Equals 和默认的相等比较器都将使用正确的相等比较器。 - DeCaf

2

有趣的问题。

中间的Equals都是true,是因为Type.Equals返回在两侧上调用UnderlyingSystemType属性时所调用的ReferenceEquals的值 - 而TypeDelegator覆盖了UnderlyingSystemType以返回您构造它时使用的Type

我不知道如何说服一个非Type等式操作理解这一点。我怀疑你做不到,你需要始终提供一个合适的EqualityComparer


1
我只是发表这个半评论半答案,以供大家感兴趣 - 我完全希望在真正的答案出现之后删除它 :) - AakashM
1
我理解你的观点,但我认为默认的相等比较器为什么会根据其参数的顺序返回不同的结果是没有道理的。在我看来,这感觉接近于.NET框架中的一个错误。(如果Type实现了IEquatable,这个问题可能已经得到解决)。 - DeCaf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接