在System.Attribute中错误地实现了GetHashCode和Equals?

时间:2012-01-12 17:26:57

标签: c# .net attributes equals gethashcode

Artech's blog看到,然后我们在评论中进行了讨论。由于该博客仅以中文撰写,我在此处作简要说明。重现的代码:

[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方法随机取一个 < / em>的)。所以问题是:实施有什么特殊原因吗?或者它只是一个错误?

2 个答案:

答案 0 :(得分:7)

明确的错误,没有。一个好主意,也许或许不是。

一件事与另一件事相等是什么意思?如果我们真的想要,我们可以变得非常哲学。

只是略显哲学,有一些事情必须坚持:

  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)
  4. 还有其他几个,但只有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. 在实践中,如果需要很长时间才能让事情瘫痪,尽管有人可能会说这里有理论与实践的对比。
    7. 对于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"匹配而不是其他。

答案 1 :(得分:0)

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

选项1,不要在Attribute类中使用inheritence,这将允许默认实现起作用。另一个选项是使用自定义比较器来确保在删除项目时使用引用相等性。您可以轻松地实现比较器。只需使用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);
    }
}