c# - 使用 [StructLayout(LayoutKind.Sequential)] 解码时 C# 在哪里存储结构的 vtable

标签 c# inheritance unmarshalling vtable

我有一个传输二进制数据的设备。为了解释数据,我定义了一个匹配数据格式的 structstruct 有一个 StuctLayoutAttributeLayoutKind.Sequential .这按预期工作,例如:

[StructLayout(LayoutKind.Sequential)]
struct DemoPlain
{
     public int x;
     public int y;
}

Marshal.OffsetOf<DemoPlain>("x");    // yields 0, as expected
Marshal.OffsetOf<DemoPlain>("y");    // yields 4, as expected
Marshal.SizeOf<DemoPlain>();         // yields 8, as expected

现在我希望将一个结构与另一个结构类似地对待,所以我试验了实现接口(interface)的结构:

interface IDemo
{
    int Product();
}


[StructLayout(LayoutKind.Sequential)]
struct DemoWithInterface: IDemo
{
     public int x;
     public int y;
     public int Product() => x * y;
}

Marshal.OffsetOf<DemoWithInterface>("x").Dump();    // yields 0
Marshal.OffsetOf<DemoWithInterface>("y").Dump();    // yields 4
Marshal.SizeOf<DemoWithInterface>().Dump();         // yields 8

令我惊讶的是,DemoWithInterface 的偏移量和大小与 DemoPlain 相同,并将相同的二进制数据从设备转换为 DemoPlain 数组DemoWithInterface 数组都可以。这怎么可能?

C++ 实现通常使用 vtable(参见 Where in memory is vtable stored?)来处理虚拟方法。我相信在接口(interface)中发布的 C# 方法和声明为 virtual 的方法类似于 C++ 中的虚拟方法,并且它需要类似于 vtable 的东西来找到正确的方法。这是正确的还是 C# 做的完全不同?如果正确,类似 vtable 的结构存储在哪里?如果不同,C# 在接口(interface)继承和虚方法方面是如何实现的?

最佳答案

基本上,“不适用”。 C# 中的结构 - 如前所述 - 不支持继承,因此不需要 v 表。

字段布局就是字段布局。它很简单:实际字段在哪里。实现接口(interface)根本不会更改字段,也不需要对布局进行任何更改。这就是大小和布局不受影响的原因。

一些结构可以(并且通常应该)覆盖的虚方法 - ToString() 等。所以你可以合法地问“那么它是如何工作的?” - 答案是:雾里看花。也称为 constrained call 。这将“虚拟调用与静态调用”的问题推迟到 JIT。 JIT 完全了解该方法是否被覆盖,并且可以发出适当的操作码 - 框和虚拟调用(框是一个对象,因此有一个 v 表),或者直接静态调用。

可能很容易认为编译器应该这样做,而不是 JIT - 但结构通常在外部程序集中,如果编译器发出静态调用,那将是灾难性的,因为它可以看到被覆盖的 ToString() 等,然后有人在不重建应用程序的情况下更新了库,并且它获得了一个不会覆盖的版本(MissingMethodException)-因此受限调用更可靠。即使对汇编内类型做同样的事情也更简单,更容易支持。

此受限调用也发生在通用 (<T>) 方法中 - 因为 T 可能是 struct。回想一下,对于泛型方法上的值类型 T,JIT 按 T 执行,因此它可以按类型应用此逻辑,并烘焙实际已知的静态调用位置。如果您使用的是 .ToString() 之类的东西,而您的 T 是一个不会覆盖它的结构:它会改为装箱和虚拟调用。

请注意,一旦您将结构分配给 interface 变量 - 例如:

DemoWithInterface foo = default;
IDemo bar = foo;
var i = bar.Product();

你已经“装箱”了它,现在一切都在盒子上进行虚拟调用。一个 box 有一个完整的 v 表。这就是为什么具有泛型类型约束的泛型方法通常更可取:

DemoWithInterface foo = default;
DoSomething(foo);

void DoSomething<T>(T obj) where T : IDemo
{
    //...
    int i = obj.Product();
    //...
}

将始终使用约束调用并且不需要框,尽管访问接口(interface)成员。 JIT 在执行时解析特定 T 的静态调用选项。

关于c# - 使用 [StructLayout(LayoutKind.Sequential)] 解码时 C# 在哪里存储结构的 vtable,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49687266/

相关文章:

java - 如何使用 JAXB 从服务返回的 'anyType' 创建 java 对象?

c# - 有条件地将 CSS 类添加到 Web 网格列项目

C# usercontrol 如何访问所有子控件

javascript - 如何在 OpenLayers 3 中使用我自己的类 myMap(使用继承)初始化 ol.map?

json - 如何在 Go 中使用非必需的 JSON 参数?

c - 解码以太网帧和数据对齐

c# - MSTEST 中的多个 TestInitialize 属性

c# - 在 C# 中更改标签的背景颜色

sql - 从父表中选择一行时显示子表名称

java - 在 Java 继承中隐藏字段