GetHashCode и Equals неправильно реализованы в System.Attribute? - PullRequest
14 голосов
/ 12 января 2012

Вид из блога Артеха , а затем у нас было обсуждение в комментариях. Поскольку этот блог написан только на китайском языке, я кратко объясню здесь. Код для воспроизведения:

[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public abstract class BaseAttribute : Attribute
{
    public string Name { get; set; }
}

public class FooAttribute : BaseAttribute { }

[Foo(Name = "A")]
[Foo(Name = "B")]
[Foo(Name = "C")]
public class Bar { }

//Main method
var attributes = typeof(Bar).GetCustomAttributes(true).OfType<FooAttribute>().ToList<FooAttribute>();
var getC = attributes.First(item => item.Name == "C");
attributes.Remove(getC);
attributes.ForEach(a => Console.WriteLine(a.Name));

Код получает все FooAttribute и удаляет тот, чье имя "C". Очевидно, что выход «А» и «Б»? Если бы все шло гладко, вы бы не увидели этот вопрос. Фактически, вы получите «AC», «BC» или даже теоретически правильный «AB» (я получил AC на своей машине, а автор блога получил BC). Проблема возникает из-за реализации GetHashCode / Equals в System.Attribute. Фрагмент реализации:

  [SecuritySafeCritical]
  public override int GetHashCode()
  {
      Type type = base.GetType();
      //*****NOTICE*****
      FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic 
            | BindingFlags.Public 
            | BindingFlags.Instance);
      object obj2 = null;
      for (int i = 0; i < fields.Length; i++)
      {
          object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(this, false, false);
          if ((obj3 != null) && !obj3.GetType().IsArray)
          {
              obj2 = obj3;
          }
          if (obj2 != null)
          {
              break;
          }
      }
      if (obj2 != null)
      {
          return obj2.GetHashCode();
      }
      return type.GetHashCode();
  }

Используется Type.GetFields, поэтому свойства, унаследованные от базового класса, игнорируются, следовательно, эквивалентность трех экземпляров FooAttribute (а затем метод Remove принимает один случайным образом ). Таким образом, вопрос: есть ли особая причина для реализации? Или это просто ошибка?

Ответы [ 2 ]

7 голосов
/ 13 января 2012

явной ошибки нет. Хорошая идея, возможно, или нет.

Что значит для одной вещи быть равной другой? Мы могли бы стать весьма философскими, если бы действительно хотели.

Будучи лишь слегка философским, есть несколько вещей, которые должны соблюдаться:

  1. Равенство рефлексивно: идентичность влечет за собой равенство. x.Equals(x) должен удержаться.
  2. Равенство симметрично. Если x.Equals(y), то y.Equals(x), а если !x.Equals(y), то !y.Equals(x).
  3. Равенство является переходным. Если x.Equals(y) и y.Equals(z), то x.Equals(z).

Есть несколько других, хотя только они могут быть непосредственно отражены кодом только для Equals().

Если реализация переопределения object.Equals(object), IEquatable<T>.Equals(T), IEqualityComparer.Equals(object, object), IEqualityComparer<T>.Equals(T, T), == или != не соответствует вышеуказанному, это явная ошибка.

Другим методом, который отражает равенство в .NET, являются object.GetHashCode(), IEqualityComparer.GetHashCode(object) и IEqualityComparer<T>.GetHashCode(T). Вот простое правило:

Если a.Equals(b), то оно должно содержать a.GetHashCode() == b.GetHashCode(). Эквивалент справедлив для IEqualityComparer и IEqualityComparer<T>.

Если этого не произойдет, то снова у нас будет ошибка.

Кроме того, не существует общих правил о том, что должно означать равенство. Это зависит от семантики класса, обеспечиваемой его собственными переопределениями Equals() или теми, которые ему навязывает компаратор равенства. Конечно, эта семантика должна быть либо явно очевидной, либо документирована в классе или в компараторе равенства.

В общем, как Equals и / или GetHashCode имеют ошибку:

  1. Если он не обеспечивает рефлексивные, симметричные и переходные свойства, описанные выше.
  2. Если отношения между GetHashCode и Equals не такие, как указано выше.
  3. Если он не соответствует задокументированной семантике.
  4. Если выдается неуместное исключение.
  5. Если он уходит в бесконечный цикл.
  6. На практике, если потребуется так много времени, чтобы вернуться, чтобы покалечить вещи, хотя можно утверждать, что здесь есть теория и практика.

С переопределениями Attribute, equals имеет рефлексивные, симметричные и транзитивные свойства, оно GetHashCode соответствует ему, и документация для его Equals переопределения:

Этот API-интерфейс поддерживает инфраструктуру .NET Framework и не предназначен для использования непосредственно из вашего кода.

Вы не можете сказать, что ваш пример это опровергает!

Поскольку код, на который вы жалуетесь, не сбоит ни в одном из этих пунктов, это не ошибка.

В этом коде есть ошибка:

var attributes = typeof(Bar).GetCustomAttributes(true).OfType<FooAttribute>().ToList<FooAttribute>();
var getC = attributes.First(item => item.Name == "C");
attributes.Remove(getC);

Сначала вы запрашиваете элемент, который удовлетворяет критериям, а затем запрашиваете элемент, равный ему, который будет удален. Нет смысла без изучения семантики равенства для рассматриваемого типа ожидать, что getC будет удалено.

Что вы должны сделать, это:

bool calledAlready;
attributes.RemoveAll(item => {
  if(!calledAlready && item.Name == "C")
  {
    return calledAlready = true;
  }
});

То есть мы используем предикат, который соответствует первому атрибуту с Name == "C" и никаким другим.

0 голосов
/ 13 января 2012

Да, ошибка, о которой другие уже упоминали в комментариях.Я могу предложить несколько возможных исправлений:

Вариант 1. Не использовать наследование в классе Attribute, это позволит реализации по умолчанию функционировать.Другой вариант - использовать пользовательский компаратор, чтобы убедиться, что при удалении элемента используется равенство ссылок.Вы можете реализовать сравнитель достаточно легко.Просто используйте Object.ReferenceEquals для сравнения и для своего использования вы можете использовать хеш-код типа или использовать System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode .

public sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T>
{
    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return Object.ReferenceEquals(x, y);
    }
    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
    }
}
...