System.Attribute 中 GetHashCode 和 Equals 的实现不正确?

发布于 2024-12-26 08:01:13 字数 1953 浏览 1 评论 0原文

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();
 //*****注意*****
      FieldInfo[] 字段 = 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方法随机获取一个)。那么问题来了:实施有什么特殊原因吗?或者这只是一个错误?

Seeing from Artech's blog and then we had a discussion in the comments. Since that blog is written in Chinese only, I'm taking a brief explanation here. Code to reproduce:

[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));

The code gets all FooAttribute and removes the one whose name is "C". Obviously the output is "A" and "B"? If everything was going smoothly you wouldn't see this question. In fact you will get "AC" "BC" or even correct "AB" theoretically (I got AC on my machine, and the blog author got BC). The problem results from the implementation of GetHashCode/Equals in System.Attribute. A snippet of the implementation:

  [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();
  }

It uses Type.GetFields so the properties inherited from base class are ignored, hence the equivalence of the three instances of FooAttribute (and then the Remove method takes one randomly). So the question is: is there any special reason for the implementation? Or it's just a bug?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

北方。的韩爷 2025-01-02 08:01:13

一个明显的错误,不。也许是个好主意,也许不是。

一件事与另一件事相等意味着什么?如果我们真的愿意的话,我们可以变得非常哲学。

虽然只是有点哲学性,但有几件事必须成立:

  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.Equals(T)IEqualityComparer.Equals(object, object)< /code>、IEqualityComparer.Equals(T, T)==!= 不满足上面,这是一个明显的错误。

.NET 中反映相等性的其他方法是 object.GetHashCode()IEqualityComparer.GetHashCode(object)IEqualityComparer.GetHashCode(T)< /代码>。这里有一个简单的规则:

如果a.Equals(b),那么它必须满足a.GetHashCode() == b.GetHashCode()IEqualityComparerIEqualityComparer 的情况相同。

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

除此之外,对于平等的含义并没有总体规则。它取决于类自身的 Equals() 覆盖提供的语义或由相等比较器强加给它的语义。当然,这些语义要么是显而易见的,要么记录在类或相等比较器中。

总而言之,Equals 和/或 GetHashCode 为何存在 bug:

  1. 如果它无法提供上面详述的自反、对称和传递属性。
  2. 如果GetHashCodeEquals之间的关系不是如上。
  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" 而没有其他属性。

A clear bug, no. A good idea, perhaps or perhaps not.

What does it mean for one thing to be equal to another? We could get quite philosophical, if we really wanted to.

Being only slightly philosophical, there are a few things that must hold:

  1. Equality is reflexive: Identity entails equality. x.Equals(x) must hold.
  2. Equality is symmetric. If x.Equals(y) then y.Equals(x) and if !x.Equals(y) then !y.Equals(x).
  3. Equality is transitive. If x.Equals(y) and y.Equals(z) then x.Equals(z).

There's a few others, though only these can directly be reflected by the code for Equals() alone.

If an implementation of an override of object.Equals(object), IEquatable<T>.Equals(T), IEqualityComparer.Equals(object, object), IEqualityComparer<T>.Equals(T, T), == or of != does not meet the above, it's a clear bug.

The other method that reflects equality in .NET are object.GetHashCode(), IEqualityComparer.GetHashCode(object) and IEqualityComparer<T>.GetHashCode(T). Here there's the simple rule:

If a.Equals(b) then it must hold that a.GetHashCode() == b.GetHashCode(). The equivalent holds for IEqualityComparer and IEqualityComparer<T>.

If that doesn't hold, then again we've got a bug.

Beyond that, there are no over-all rules on what equality must mean. It depends on the semantics of the class provided by its own Equals() overrides or by those imposed upon it by an equality comparer. Of course, those semantics should either be blatantly obvious or else documented in the class or the equality comparer.

In all, how does an Equals and/or a GetHashCode have a bug:

  1. If it fails to provide the reflexive, symmetric and transitive properties detailed above.
  2. If the relationship between GetHashCode and Equals is not as above.
  3. If it doesn't match its documented semantics.
  4. If it throws an inappropriate exception.
  5. If it wanders off into an infinite loop.
  6. In practice, if it takes so long to return as to cripple things, though one could argue there's a theory vs. practice thing here.

With the overrides on Attribute, the equals does have the reflexive, symmetric and transitive properties, it's GetHashCode does match it, and the documentation for it's Equals override is:

This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.

You can't really say your example disproves that!

Since the code you complain about doesn't fail on any of these points, it's not a bug.

There's a bug though in this code:

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

You first ask for an item that fulfills a criteria, and then ask for one that is equal to it to be removed. There's no reason without examining the semantics of equality for the type in question to expect that getC would be removed.

What you should do is:

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

That is to say, we use a predicate that matches the first attribute with Name == "C" and no other.

染年凉城似染瑾 2025-01-02 08:01:13

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

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

Yep, a bug as others have already mentioned in the comments. I can suggest a few possible fixes:

Option 1, Don't use inheritence in the Attribute class, this will allow the default implementation to function. The other option is use a custom comparer to ensure you are using reference equality when removing the item. You can implement a comparer easily enough. Just use Object.ReferenceEquals for comparison and for your use you could use the type's hash code or use 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);
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文