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(。该问题是由于在System.Attribute中实现GetHashCode/Equals引起的。实现的一个片段:

  [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)必须坚持。
  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()。等效适用于IEqualityComparerIEqualityComparer<T>

如果这不成立,那么我们又遇到了一个错误。

除此之外,没有关于平等必须意味着什么的全面规则。它取决于类自己的Equals()覆盖或平等比较者强加给它的语义所提供的语义。当然,这些语义要么是显而易见的,要么应该记录在类或相等比较器中。

总而言之,Equals和/或GetHashCode如何出现错误:

  1. 如果它无法提供上面详述的自反、对称和传递属性。
  2. 如果GetHashCodeEquals之间的关系与上述不同。
  3. 如果它与记录的语义不匹配。
  4. 如果它引发不适当的异常。
  5. 如果它徘徊在一个无限循环中。
  6. 在实践中,如果恢复到使事情瘫痪需要很长时间,尽管有人可能会争辩说这里有理论与实践的东西。

对于 Attribute 的覆盖,等号确实具有自反、对称和传递属性,它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"属性匹配,而没有其他属性。

是的,正如其他人已经在评论中提到的错误。 我可以建议一些可能的修复方法:

选项 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);
    }
}

最新更新