请注意,此通用方法仅使用<U>
参数化,表示指向的值的类型,而不是包含类(可能是“<T>
”)的类型。这是因为这种技术的基本简单性不需要它。我们已经知道包含实例必须是引用(class
)类型,因此在运行时,它将通过引用句柄呈现为具有object
头的GC对象,并且这些事实足够了;没有进一步了解假定类型“T
”。
是否添加空洞的<T,...>
,这将允许我们指示where T:class
约束条件,这是一个观点问题,是否会改善上面示例的外观或感觉。当然不会有任何损害;我相信JIT足够聪明,不会为没有影响的通用参数生成额外的通用方法实例化。但是,既然这样做似乎很啰嗦(除了陈述约束条件之外),我选择了这里严格必要性的极简主义。
在我的使用中,我实际保留的是各个感兴趣的字段的整数偏移值(从GetFieldOffset
返回),而不是每次传递FieldInfo
或其相应的FieldHandle
。因为一旦获取到这些偏移值,它们在运行时也是不变的。这消除了每次获取指针时调用GetFieldOffset
的额外步骤。事实上,由于我能够在我的项目中包含IL代码,这里是我用于上述函数的精确代码。与刚才展示的C#一样,它轻松地从包含GC对象obj
和其中的(保留的)整数偏移量offs
合成托管指针。
// Returns a managed 'ByRef' pointer to the (struct or reference-type) instance of type U
// stored in the field at byte offset 'offs' within reference type instance 'obj'
.method public static !!U& RefFieldValue<U>(object obj, int32 offs) aggressiveinlining
{
ldarg obj
ldarg offs
sizeof object
add
add
ret
}
因此,即使您无法直接将此IL纳入其中,展示它在这里,我认为可以很好地说明这种技术的运行时开销极低和普遍的诱人简单性。
示例用法
class MyClass { public byte b_bar; public String s0, s1; public int iFoo; }
第一个演示获取MyClass
实例中引用类型字段s1
的整数偏移量,然后使用它来获取和设置字段值。
var fi = typeof(MyClass).GetField("s1");
var offs = GetFieldOffset(fi);
var mc = new MyClass();
RefFieldValue<String>(mc, offs) = "moo-maa";
RefFieldValue<String>(mc, offs) += "!!";
Console.WriteLine(mc.s1);
var _ = RefFieldValue<String>(mc, offs) + "%%";
如果这看起来有点凌乱,您可以通过将托管指针保留为 ref local 变量来大幅简化它。正如您所知,这种类型的指针会自动调整 - 保留内部偏移 - 每当 GC 移动 包含 对象时。这意味着即使您继续不知不觉地访问该字段,它仍将保持有效。作为交换,CLR 要求 ref
局部变量 本身 不被允许逃离其堆栈帧,在这种情况下由 C# 编译器强制执行。
var h = typeof(MyClass).GetField(nameof(mc.iFoo)).FieldHandle;
ref int i = ref RefFieldValue<int>(mc, h);
i = 21;
Console.WriteLine(mc.iFoo == 21);
i <<= 1;
Console.WriteLine(mc.iFoo == 42);
Interlocked.CompareExchange(ref i, 34, 42);
摘要
使用示例聚焦于使用技术与class
对象一起使用,但是请注意,此处显示的GetFieldOffset
方法也可以完美地与struct
一起使用。只需确保不要对值类型使用RefFieldValue
方法,因为该代码包括调整预期对象头。对于这种更简单的情况,只需使用System.Runtime.CompilerServicesUnsafe.AddByteOffset
进行地址算术运算即可。
不用说,这种技术可能对某些人来说似乎有点激进。我只想指出,在.NET Framework 4.7.2上,包括32位和64位模式、调试与发布以及我尝试过的任何各种JIT优化设置中,它已经完美地为我工作了多年。
Field3
字段。实际上,出乎我的意料,它被重新排序了。它被移动到类的末尾,并填充(debug 和 release, 32-bit)。这可能与无法获取通用类型指针有关。 - IS4