GetHashCode и Equals неправильно реализованы в System.Attribute?

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

[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". Очевидно, что выход "A" и "B"? Если все будет идти гладко, вы не увидите этого вопроса. Фактически вы получите "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 принимает одно случайное значение). Поэтому возникает вопрос: есть ли какая-то особая причина для реализации? Или это просто ошибка?

Ответы

Ответ 1

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

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

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

  • Равенство рефлексивно: Идентичность влечет за собой равенство. x.Equals(x) должен выполняться.
  • Равенство симметрично. Если x.Equals(y), то y.Equals(x), а если !x.Equals(y), то !y.Equals(x).
  • Равенство транзитивно. Если 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 и/или a GetHashCode имеют ошибку:

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

С переопределениями на 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" и никаким другим.

Ответ 2

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

Вариант 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);
    }
}