GetHashCode 和 Equals 在 System.Attribute 中实现不正确
本文关键字:实现 不正确 Attribute System Equals GetHashCode | 更新日期: 2023-09-27 18:20:12
从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
方法随机获取一个(。那么问题来了:实施有什么特殊的原因吗?或者它只是一个错误?
一个明显的错误,不。一个好主意,也许也可能不是。
一件事等于另一件事意味着什么?如果我们真的愿意,我们可以变得非常哲学化。
只是稍微有点哲学,有几件事必须成立:
- 平等
- 是反身的:身份意味着平等。
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
和/或GetHashCode
如何出现错误:
- 如果它无法提供上面详述的自反、对称和传递属性。
- 如果
GetHashCode
和Equals
之间的关系与上述不同。 - 如果它与记录的语义不匹配。
- 如果它引发不适当的异常。
- 如果它徘徊在一个无限循环中。
- 在实践中,如果恢复到使事情瘫痪需要很长时间,尽管有人可能会争辩说这里有理论与实践的东西。
对于 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);
}
}