Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reflection Renders HashCode Unstable

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;            
}

Update:

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

Update 2:

This issue has now been addressed in dotnetcore: https://github.com/dotnet/coreclr/pull/13892

like image 878
user495625 Avatar asked Aug 31 '26 17:08

user495625


1 Answers

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:

  1. Enumerate the fields of your attribute
  2. Find the first field that isn't an array and hasn't a null value
  3. Return the hashcode of this field

We can make two conclusions at that point:

  1. Only one field is taken into account to compute the hashcode of the attribute
  2. The algorithm relies heavily on the order of the fields returned by 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:

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

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

like image 161
Kevin Gosse Avatar answered Sep 02 '26 06:09

Kevin Gosse