在下面的代码中,访问SomeClass
的自定义属性将导致SomeAttribute
的哈希函数变得不稳定。
这是怎么回事?
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;
}
更新:
现在,这已作为错误报告给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
最佳答案
这真的很棘手。首先,让我们看一下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();
}
简而言之,它的作用是:
枚举属性的字段
查找第一个不是数组且没有空值的字段
返回此字段的哈希码
在这一点上,我们可以得出两个结论:
仅考虑一个字段来计算属性的哈希码
该算法在很大程度上依赖于
Type.GetFields
返回的字段的顺序(因为我们采用匹配条件的第一个字段)进一步测试,我们可以看到
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);
}
如果第一行未注释,则代码显示:
场2
栏位1
如果该行被注释,代码将显示:
栏位1
场2
因此,它确认某些内容正在更改字段的顺序,从而为
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);
}
此代码显示:
栏位1
场2
0
0
0
场2
栏位1
剩下的唯一问题是:为什么在第一次调用
GetFields
之后字段的顺序会发生变化?我相信这与Type
实例中的内部缓存有关。我们可以通过在quickwatch窗口中运行它来检查缓存的值:
System.Runtime.InteropServices.GCHandle.InternalGet((((System.RuntimeType)typeof(SomeAttribute))。m_cache)作为RuntimeType.RuntimeTypeCache
在执行的最开始,缓存是空的(显然)。然后,我们执行:
typeof(SomeClass).GetCustomAttributes(false)
在此行之后,如果我们检查缓存,则它包含一个字段:
field2
。现在很有趣。为什么是这个领域?因为使用它,所以SomeClass
的属性为:[SomeAttribute(field2 = 1)]
然后,我们执行第一个
GetHashCode
并检查缓存,它现在包含field2
然后是field1
(请记住顺序很重要)。由于字段的顺序,随后执行GetHashCode
将返回0。现在,如果我们删除行
typeof(SomeClass).GetCustomAttributes(false)
并检查第一个GetHashCode
之后的缓存,则找到field1
,然后找到field2
。总结一下:
属性的哈希码算法使用它找到的第一个字段的值。因此,它在很大程度上依赖于
Type.GetFields
方法返回的字段的顺序。为了提高性能,此方法在内部使用缓存。有两种情况:
不使用
typeof(SomeClass).GetCustomAttributes(false);
的情况在这里,当调用
GetFields
时,缓存为空。它将由属性的字段填充,顺序为field1, field2
。然后GetHashCode
将找到field1
作为第一个字段,并显示1234567
。您使用
typeof(SomeClass).GetCustomAttributes(false);
的方案执行该行时,将执行属性构造函数:
[SomeAttribute(field2 = 1)]
。那时,field2
的元数据将被推入缓存。然后,调用GetHashCode
,缓存将完成。 field2
已经存在,因此不会再次添加。然后,接下来将添加field1
。因此,缓存中的顺序为field2, field1
。因此,GetHashCode
将找到field2
作为第一个字段,并显示0
。剩下的唯一令人惊讶的观点是:为什么第一次呼叫
GetHashCode
的行为与以后的呼叫不同?我没有检查,但是我相信它会检测到缓存不完整,并以其他方式读取字段。然后,对于后续调用,缓存将完成并且其行为将保持一致。老实说,我认为这是一个错误。
GetHashCode
的结果应随时间保持一致。因此,Attribute.GetHashCode
的实现不应依赖于Type.GetFields
返回的字段的顺序,因为我们已经看到它可以更改。这应该报告给Microsoft。关于c# - 反射使HashCode不稳定,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/43028703/