In the following code, accessing the custom attributes of a SomeClass results in the hash function of SomeAttribute becoming unstable.
What's going on?
static void Main(string[] args)
{
typeof(SomeClass).GetCustomAttributes(false);//without this line, GetHashCode behaves as expected
SomeAttribute tt = new SomeAttribute();
Console.WriteLine(tt.GetHashCode());//Prints 1234567
Console.WriteLine(tt.GetHashCode());//Prints 0
Console.WriteLine(tt.GetHashCode());//Prints 0
}
[SomeAttribute(field2 = 1)]
class SomeClass
{
}
class SomeAttribute : System.Attribute
{
uint field1=1234567;
public uint field2;
}
This has now been reported to MS as a bug. https://connect.microsoft.com/VisualStudio/feedback/details/3130763/attibute-gethashcode-unstable-if-reflection-has-been-used
This issue has now been addressed in dotnetcore: https://github.com/dotnet/coreclr/pull/13892
This one is really tricky. First, let's have a look to the source code of the Attribute.GetHashCode method:
public override int GetHashCode()
{
Type type = GetType();
FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Object vThis = null;
for (int i = 0; i < fields.Length; i++)
{
// Visibility check and consistency check are not necessary.
Object fieldValue = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);
// The hashcode of an array ignores the contents of the array, so it can produce
// different hashcodes for arrays with the same contents.
// Since we do deep comparisons of arrays in Equals(), this means Equals and GetHashCode will
// be inconsistent for arrays. Therefore, we ignore hashes of arrays.
if (fieldValue != null && !fieldValue.GetType().IsArray)
vThis = fieldValue;
if (vThis != null)
break;
}
if (vThis != null)
return vThis.GetHashCode();
return type.GetHashCode();
}
In a nutshell, what it does is:
We can make two conclusions at that point:
Type.GetFields (since we take the first field that matches the conditions)Testing further, we can see that the order of the fields returned by Type.GetFields changes between the two versions of the code:
typeof(SomeClass).GetCustomAttributes(false);//without this line, GetHashCode behaves as expected
SomeAttribute tt = new SomeAttribute();
Console.WriteLine(tt.GetHashCode());//Prints 1234567
Console.WriteLine(tt.GetHashCode());//Prints 0
Console.WriteLine(tt.GetHashCode());//Prints 0
foreach (var field in new SomeAttribute().GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
Console.WriteLine(field.Name);
}
If the first line is uncommented, the code displays:
field2
field1
If the line is commented, the code displays:
field1
field2
So it confirms that something is changing the order of the fields, thus producing different results for the GetHashCode function.
Even more interesting is this:
typeof(SomeClass).GetCustomAttributes(false);//without this line, GetHashCode behaves as expected
SomeAttribute tt = new SomeAttribute();
foreach (var field in new SomeAttribute().GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
Console.WriteLine(field.Name);
}
Console.WriteLine(tt.GetHashCode());//Prints 0
Console.WriteLine(tt.GetHashCode());//Prints 0
Console.WriteLine(tt.GetHashCode());//Prints 0
foreach (var field in new SomeAttribute().GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
Console.WriteLine(field.Name);
}
This code displays:
field1
field2
0
0
0
field2
field1
The only question left is: why is the order of the fields changing after the first call to GetFields? I believe it has something to do with an internal cache in the Type instance.
We can check the value of the cache by running this in the quickwatch window:
System.Runtime.InteropServices.GCHandle.InternalGet(((System.RuntimeType)typeof(SomeAttribute)).m_cache) as RuntimeType.RuntimeTypeCache
At the very beginning of the execution, the cache is empty (obviously). Then, we execute:
typeof(SomeClass).GetCustomAttributes(false)
After this line, if we check the cache, it contains a single field: field2. Now that's interesting. Why this field? Because you use it the attribute of SomeClass: [SomeAttribute(field2 = 1)]
Then, we execute the first GetHashCode and check the cache, it now contains field2 then field1 (remember that the order is important). Subsequent execution of GetHashCode will return 0 because of the order of the fields.
Now, if we remove the line typeof(SomeClass).GetCustomAttributes(false) and check the cache after the first GetHashCode, we find field1 then field2.
Summing it up:
The hashcode algorithm of the Attribute uses the value of the first field it finds. It therefore relies heavily on the order of the field returned by the Type.GetFields method. This method internally uses a cache, for performance purposes.
There are two scenarios:
The scenario where you don't use typeof(SomeClass).GetCustomAttributes(false);
Here, when GetFields is called, the cache is empty. It will be populated by the fields of the attribute, in the order field1, field2. Then GetHashCode will find field1 as the first field, and display 1234567.
The scenario where you use typeof(SomeClass).GetCustomAttributes(false);
When executing that line, the attribute constructor will be executed: [SomeAttribute(field2 = 1)]. At that point, the metadata of field2 will be pushed into the cache. Then you call GetHashCode, and the cache will be completed. field2 is already there, so it won't be added again. Then, field1 will be added next. So the order in the cache is field2, field1. Therefore, GetHashCode will find field2 as the first field, and display 0.
The only surprising point left is: why is the first call to GetHashCode behaving differently than the next ones? I haven't checked, but I believe that it detects that the cache is incomplete, and reads the fields in a different fashion. Then for subsequent calls the cache is complete and it behaves consistently.
Honestly, I think this is a bug. The results of GetHashCode should be consistent over time. Therefore, the implementation of Attribute.GetHashCode shouldn't rely on the order of the fields returned by Type.GetFields, as we've seen that it can change. This should be reported to Microsoft.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With