System.Attribute 中 GetHashCode 和 Equals 的实现不正确?
从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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
一个明显的错误,不。也许是个好主意,也许不是。
一件事与另一件事相等意味着什么?如果我们真的愿意的话,我们可以变得非常哲学。
虽然只是有点哲学性,但有几件事必须成立:
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.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()
。IEqualityComparer
和IEqualityComparer
的情况相同。如果这不成立,那么我们又遇到了一个错误。
除此之外,对于平等的含义并没有总体规则。它取决于类自身的
Equals()
覆盖提供的语义或由相等比较器强加给它的语义。当然,这些语义要么是显而易见的,要么记录在类或相等比较器中。总而言之,
Equals
和/或GetHashCode
为何存在 bug:GetHashCode
和Equals
之间的关系不是如上。通过对
Attribute
的覆盖,equals 确实具有自反、对称和传递属性,它的GetHashCode
确实与它匹配,并且它的文档是Equals
覆盖是:你不能说你的例子反驳了这一点!
由于您抱怨的代码在这些方面都没有失败,所以这不是一个错误。
但此代码中存在一个错误:
您首先请求满足条件的项目,然后请求删除与其相等的项目。如果不检查相关类型的相等语义,就没有理由期望
getC
将被删除。你应该做的是:
也就是说,我们使用一个谓词来匹配第一个属性
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:
x.Equals(x)
must hold.x.Equals(y)
theny.Equals(x)
and if!x.Equals(y)
then!y.Equals(x)
.x.Equals(y)
andy.Equals(z)
thenx.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)
andIEqualityComparer<T>.GetHashCode(T)
. Here there's the simple rule:If
a.Equals(b)
then it must hold thata.GetHashCode() == b.GetHashCode()
. The equivalent holds forIEqualityComparer
andIEqualityComparer<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 aGetHashCode
have a bug:GetHashCode
andEquals
is not as above.With the overrides on
Attribute
, the equals does have the reflexive, symmetric and transitive properties, it'sGetHashCode
does match it, and the documentation for it'sEquals
override is: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:
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:
That is to say, we use a predicate that matches the first attribute with
Name == "C"
and no other.是的,正如其他人在评论中已经提到的那样,这是一个错误。我可以建议一些可能的修复:
选项 1,不要在 Attribute 类中使用继承,这将允许默认实现发挥作用。另一个选项是使用自定义比较器来确保在删除项目时使用引用相等。您可以很容易地实现比较器。只需使用 Object.ReferenceEquals 进行比较,并且您可以使用类型的哈希代码或使用 System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode。
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.