Непоследовательность равенства типаDelegator?
Рассмотрим следующий код:
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
.
Ответы
Ответ 1
Следующий код возвращает 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 для прямого сравнения... Казалось бы, нет простого способа решить проблему (без предоставления сравнения).
ИЗМЕНИТЬ ДОБАВИТЬ:
Из любопытства я рассмотрел реализацию RuntimeType.NET 1.0 и 1.1. У них нет переопределения Equals в RuntimeType, поэтому проблема была внедрена в .NET 2.0.
Ответ 2
Update
Код из этого ответа стал репозиторием 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. класс Configurator
Этот класс использует отражение для настройки базового поля для EqualityComparer<T>.Default
. В качестве бонуса этот класс предоставляет механизм для управления значением Comparer<T>.Default
, а также гарантирует совместимость результатов конфигурированных реализаций. Существует также способ возврата конфигураций к значениям по умолчанию Framework.
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); }
}
Ответ 3
Увлекательный q.
Средние Equals
оба являются true
, потому что Type.Equals
возвращает значение ReferenceEquals
как вызванное в свойстве UnderlyingSystemType
для обеих сторон - и TypeDelegator
переопределяет UnderlyingSystemType
, чтобы вернуть Type
вы построили его с помощью
Как вы можете убедить в том, что я не знаю. Я подозреваю, что вы не можете, и вам всегда нужно знать подходящий EqualityComparer
.
Ответ 4
EqualityComparer<T>
defauls to object.Equals метод, поэтому 1) и 2) случаи эквивалентны 5) и 6).
Я не понимаю, почему это сравнение должно быть последовательным по умолчанию. Истинные случаи случаются, потому что реализация равенства System.Type
основана на свойстве UnderlyingSystemType
. Таким образом, вы можете переопределить Equals (object) и Equals (Type) - BTW, виртуальные только на Framework 4 -, но это не исправило бы случай 3).
Итак, что вы можете сделать, чтобы убедиться, что оно последовательное:
class MyType : TypeDelegator
{
public MyType(Type parent)
: base(parent)
{
}
public override Type UnderlyingSystemType
{
get
{
return this;
}
}
}
С этой реализацией все случаи сообщают об ошибке, что согласуется, но я не уверен в побочных эффектах... Я полагаю, это зависит от того, что ваш код в конечном итоге.