use*_*625 3 c# reflection hash custom-attributes
在以下代码中,访问 a 的自定义属性会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;
}
Run Code Online (Sandbox Code Playgroud)
现在已将此作为错误报告给 MS。 https://connect.microsoft.com/VisualStudio/feedback/details/3130763/attibute-gethashcode-unstable-if-reflection-has-been-used
此问题现已在 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();
}
Run Code Online (Sandbox Code Playgroud)
简而言之,它的作用是:
此时我们可以得出两个结论:
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);
}
Run Code Online (Sandbox Code Playgroud)
如果第一行没有注释,代码显示:
字段 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);
}
Run Code Online (Sandbox Code Playgroud)
此代码显示:
字段 1
字段 2
0
0
0
字段 2
字段 1
剩下的唯一问题是:为什么在第一次调用后字段的顺序会发生变化GetFields?我相信这与Type实例中的内部缓存有关。
我们可以通过在 quickwatch 窗口中运行它来检查缓存的值:
System.Runtime.InteropServices.GCHandle.InternalGet(((System.RuntimeType)typeof(SomeAttribute)).m_cache) as RuntimeType.RuntimeTypeCache
在执行的最开始,缓存是空的(显然)。然后,我们执行:
typeof(SomeClass).GetCustomAttributes(false)
Run Code Online (Sandbox Code Playgroud)
在这一行之后,如果我们检查缓存,它包含一个字段:field2。现在这很有趣。为什么是这个领域?因为你使用它的属性SomeClass:[SomeAttribute(field2 = 1)]
然后,我们执行第一个GetHashCode并检查缓存,它现在包含field2then field1(记住顺序很重要)。GetHashCode由于字段的顺序,后续执行将返回 0。
现在,如果我们删除该行typeof(SomeClass).GetCustomAttributes(false)并在 first 之后检查缓存GetHashCode,我们会找到field1then 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将 findfield2作为第一个字段,并显示0。
剩下的唯一令人惊讶的一点是:为什么第一个调用的GetHashCode行为与下一个调用的行为不同?我没有检查,但我相信它检测到缓存不完整,并以不同的方式读取字段。然后对于后续调用,缓存是完整的并且它的行为一致。
老实说,我认为这是一个错误。GetHashCode随着时间的推移,结果应该是一致的。因此, 的实现不Attribute.GetHashCode应该依赖于返回的字段的顺序Type.GetFields,因为我们已经看到它可以改变。这应该报告给微软。
| 归档时间: |
|
| 查看次数: |
231 次 |
| 最近记录: |