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?
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.