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

Che*_*hen 14 .net c# attributes equals gethashcode

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));
Run Code Online (Sandbox Code Playgroud)

代码全部获取FooAttribute并删除名称为"C"的代码.显然输出是"A"和"B"?如果一切顺利,你就不会看到这个问题.事实上,理论上你会得到"AC""BC"甚至是"AB"(我的机器上有AC,博客作者有BC).问题源于System.Attribute中GetHashCode/Equals的实现.实现的片段:

  [SecuritySafeCritical]
  public override int GetHashCode()
  {
      Type type = base.GetType();
Run Code Online (Sandbox Code Playgroud)
      //*****NOTICE*****
      FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic 
            | BindingFlags.Public 
            | BindingFlags.Instance);
Run Code Online (Sandbox Code Playgroud)
      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();
  }
Run Code Online (Sandbox Code Playgroud)

它使用Type.GetFields,因此忽略从基类继承的属性,因此三个实例的等价性FooAttribute(然后该Remove方法随机取一个).所以问题是:实施有什么特殊原因吗?或者它只是一个错误?

Jon*_*nna 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).

还有其他一些,但只有这些可以直接反映在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有bug的方式:

  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);
Run Code Online (Sandbox Code Playgroud)

您首先要求一个符合标准的项目,然后要求删除一个等于它的项目.没有理由不检查相关类型的相等语义,以期望getC将被删除.

你应该做的是:

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

也就是说,我们使用与第一个属性匹配Name == "C"而不与其他属性匹配的谓词.