TypeDelegator несоответствие равенства? - PullRequest
22 голосов
/ 03 октября 2011

Рассмотрим следующий код:

    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.

Это, очевидно, создает проблемы, когда метод возвращает Type, который на самом деле наследует от TypeDelegator.Представьте, например, размещение этих типов как ключей в словаре, который по умолчанию использует EqualityComparer.Default для сравнения.

Есть ли способ решить эту проблему?Я хотел бы, чтобы все методы в приведенном выше коде возвращали true.

Ответы [ 4 ]

7 голосов
/ 16 октября 2011

Следующий код возвращает System.RuntimeType

Type t1 = typeof(string);

Если вы посмотрите на код для типа, там есть:

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 для прямого сравнения ... Казалось бы, нет простого способа обойти эту проблему (без предоставления компаратора).

ИЗМЕНЕНО ДЛЯ ДОБАВЛЕНИЯ: Из любопытства я взглянул на реализацию RuntimeType в .NET 1.0 и 1.1. У них нет переопределения Equals в RuntimeType, поэтому проблема была представлена ​​в .NET 2.0.

4 голосов
/ 26 ноября 2012

Обновление

Код из этого ответа стал хранилищем на GitHub: Undefault.NET на GitHub

Стивен дает хорошее объяснение того, почему это работает так, как работает. Я не верю, что есть решение для дела 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. Конфигуратор класс

Этот класс использует отражение для настройки базового поля для 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>, который обеспечивает совместимость между Comparer<T> и EqualityComparer<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 голосов
/ 09 октября 2011

EqualityComparer<T> по умолчанию для объекта. Метод равенства, поэтому случаи 1) и 2) эквивалентны 5) и 6).

Я не понимаю, почему эти сравнения должны быть последовательными по умолчанию. Истинные случаи происходят потому, что реализация равенства System.Type основана на свойстве UnderlyingSystemType. Таким образом, вы можете переопределить Equals (объект) и Equals (Type) - BTW, виртуальные только в Framework 4 -, но это не исправит случай 3).

Итак, что вы можете сделать, чтобы убедиться, что оно соответствует:

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

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

С этой реализацией все случаи будут сообщать ложное, что является непротиворечивым, но я не уверен насчет побочных эффектов ... Я полагаю, это зависит от того, что ваш код в конечном итоге делает.

2 голосов
/ 03 октября 2011

Увлекательный q.

Середина Equals, являющаяся true, обусловлена ​​тем, что Type.Equals возвращает значение ReferenceEquals, которое вызывается для свойства UnderlyingSystemType для обеих сторон, и TypeDelegator переопределяет UnderlyingSystemType для возврата Type Вы построили это с!

Как вы можете убедить не-1013 * -йскую операцию равенства, чтобы понять это, я не знаю. Я подозреваю, что вы не можете, и вам всегда нужно будет предоставить подходяще осведомленного EqualityComparer.

...