我有一个传输二进制数据的设备。为了解释数据,我定义了一个匹配数据格式的 struct
。 struct
有一个 StuctLayoutAttribute
与 LayoutKind.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/