反射呈现HashCode不稳定

时间:2017-03-26 12:29:39

标签: c# reflection hash custom-attributes

在以下代码中,访问[HttpPut]的自定义属性会导致[HttpDelete]的哈希函数变得不稳定。 发生了什么事?

SomeClass

更新

现在已向MS报告此错误。 https://connect.microsoft.com/VisualStudio/feedback/details/3130763/attibute-gethashcode-unstable-if-reflection-has-been-used

更新2:

此问题现已在dotnetcore中解决: https://github.com/dotnet/coreclr/pull/13892

2 个答案:

答案 0 :(得分:4)

这个真的很棘手。首先,让我们看一下Attribute.GetHashCode方法的源代码:

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

简而言之,它的作用是:

  1. 枚举属性的字段
  2. 找到第一个不是数组且没有空值的字段
  3. 返回此字段的哈希码
  4. 我们可以在这一点上得出两个结论:

    1. 仅计算一个字段来计算属性
    2. 的哈希码
    3. 该算法在很大程度上依赖于Type.GetFields返回的字段的顺序(因为我们采用符合条件的第一个字段)
    4. 进一步测试,我们可以看到Type.GetFields返回的字段顺序在两个版本的代码之间发生了变化:

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

      如果第一行被取消注释,代码将显示:

        

      FIELD2

           

      FIELD1

      如果对该行进行了注释,则代码显示:

        

      FIELD1

           

      FIELD2

      因此,它确认某些内容正在改变字段的顺序,从而为GetHashCode函数产生不同的结果。

      更有趣的是:

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

      此代码显示:

        

      FIELD1

           

      FIELD2

           

      0

           

      0

           

      0

           

      FIELD2

           

      FIELD1

      唯一的问题是:为什么第一次调用GetFields后字段的顺序会发生变化?我认为它与Type实例中的内部缓存有关。

      我们可以通过在快速监视窗口中运行来检查缓存的值:

        

      System.Runtime.InteropServices.GCHandle.InternalGet(((System.RuntimeType)typeof(SomeAttribute))。m_cache)as RuntimeType.RuntimeTypeCache

      在执行的最开始,缓存是空的(显然)。然后,我们执行:

      typeof(SomeClass).GetCustomAttributes(false)
      

      在此行之后,如果我们检查缓存,它将包含一个字段:field2。现在这很有趣。为什么这个领域?因为您使用了SomeClass[SomeAttribute(field2 = 1)]

      的属性

      然后,我们执行第一个GetHashCode并检查缓存,它现在包含field2然后field1(请记住顺序很重要)。由于字段的顺序,后续执行GetHashCode将返回0。

      现在,如果我们删除第typeof(SomeClass).GetCustomAttributes(false)行并在第一个GetHashCode之后检查缓存,我们会找到field1,然后field2

      总结:

      Attribute的哈希码算法使用它找到的第一个字段的值。因此,它在很大程度上依赖于Type.GetFields方法返回的字段的顺序。出于性能目的,此方法在内部使用缓存。

      有两种情况:

      1. 您不使用typeof(SomeClass).GetCustomAttributes(false);

        的情况

        这里,当调用GetFields时,缓存为空。它将按属性的字段填充,顺序为field1, field2。然后GetHashCode会将field1作为第一个字段,并显示1234567

      2. 您使用typeof(SomeClass).GetCustomAttributes(false);

        的方案

        执行该行时,将执行属性构造函数:[SomeAttribute(field2 = 1)]。此时,field2的元数据将被推送到缓存中。然后调用GetHashCode,缓存将完成。 field2已经存在,因此不会再添加。然后,接下来会添加field1。因此,缓存中的顺序为field2, field1。因此,GetHashCode会将field2作为第一个字段,并显示0

      3. 唯一令人惊讶的一点是:为什么第一次调用GetHashCode的行为与下一次行为不同?我没有检查过,但我相信它检测到缓存不完整,并以不同的方式读取字段。然后,对于后续调用,缓存完成并且行为一致。

        老实说,我认为这是一个错误。 GetHashCode的结果应该随着时间的推移而保持一致。因此,Attribute.GetHashCode的实现不应该依赖Type.GetFields返回的字段的顺序,因为我们已经看到它可以更改。这应该报告给微软。

答案 1 :(得分:0)

凯文对此的出色分析。我认为框架实现应该使用所有字段和属性的类型来计算哈希码,并且显然每次都生成相同的哈希码。同时这里有2个解决方案。我不是计算/组合哈希码的专家,所以我使用一个用于元组。

class SomeAttribute : System.Attribute
{
    uint field1 = 1234567;
    public uint field2;

    public override int GetHashCode()
    {
        return (GetType(), field1, field2).GetHashCode();
    }
}

另一种解决方案,如果您希望每个实例都是唯一的(可在字典中使用)。在Object上使用GetHashCode。

class SomeAttribute : System.Attribute
{
    private object FixHashCodeBug = new Object();

    public override int GetHashCode()
    {
        return FixHashCodeBug.GetHashCode();
    }
}