我有一个传输二进制数据的设备。为了解释数据,我定义了一个与数据格式匹配的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 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
数组{1}}两者都有效。这怎么可能?
C ++实现通常使用vtable(请参阅Where in memory is vtable stored?)来解决虚拟方法。我相信在接口中发布的C#方法和声明为virtual
的方法类似于C ++中的虚方法,并且它需要类似于vtable的东西才能找到正确的方法。这是正确的还是C#完全不同?如果正确,vtable结构存储在哪里?如果不同,关于接口继承和虚方法如何实现C#?
答案 0 :(得分:5)
基本上,&#34;不适用&#34;。 C#中的结构 - 如前所述 - 不支持继承,因此不需要v-table。
字段布局是字段布局。它很简单:实际字段在哪里。实现接口根本不会改变字段,也不需要对布局进行任何更改。这就是为什么尺寸和布局没有受到影响的原因。
是一些结构可以(通常应该)覆盖的虚拟方法 - ToString()
等等。所以你可以合法地询问&#34;那么它是如何工作的?&#34 ; - 答案是:烟雾和镜子。也称为constrained call。这推迟了&#34;虚拟呼叫与静态呼叫的问题&#34;到JIT。 JIT完全知道该方法是否被覆盖,并且可以发出适当的操作码 - 盒子和虚拟调用(盒子是对象,所以有v-table),或者直接静态调用。“ p>
可能很容易认为编译器应该这样做,而不是JIT - 但结构通常是在外部程序集中,如果编译器发出静态调用,它会是灾难性的,因为它可以看到被覆盖的{{ 1}}等等,然后有人在不重建应用程序的情况下更新库,并且它获得不覆盖(ToString()
)的版本 - 因此受限制的调用更可靠。即使对于嵌入式类型,做同样的事情也更简单,更容易支持。
此约束调用也适用于通用(MissingMethodException
)方法 - 因为<T>
可能是T
。回想一下,对于泛型方法,JIT对值类型struct
执行T
,因此它可以应用每个类型的逻辑,并在实际已知的静态调用位置进行烘焙。如果您正在使用T
这样的内容,而您的.ToString()
是一个不会覆盖它的结构:它将改为包装和虚拟调用。
注意,一旦将结构分配给接口变量 - 例如:
T
你有&#34;盒装&#34;它,现在一切都是虚拟呼叫。 框具有完整的v表。这就是为什么具有泛型类型约束的泛型方法通常更可取的原因:
DemoWithInterface foo = default;
IDemo bar = foo;
var i = bar.Product();
尽管访问接口成员,将始终使用受约束的呼叫,并且不需要盒子。 JIT在执行时解析特定DemoWithInterface foo = default;
DoSomething(foo);
void DoSomething<T>(T obj) where T : IDemo
{
//...
int i = obj.Product();
//...
}
的静态调用选项。
答案 1 :(得分:0)
Default Marshalling Behavior | Microsoft Docs,尤其是Value Types Used in Platform Invoke部分提供了答案:
当封送到非托管代码时,这些格式化的类型被封送为C样式结构。
和
当格式化的类型作为结构封送时,只能访问该类型中的字段。如果类型具有方法,属性或事件,则无法从非托管代码中访问它们。
因此,C#struct
的(虚拟)方法被剥离,并且只传输了一个普通的C-struct。在OP的情况下,设备发送包含普通C结构的字节,Marshal.PtrToStructure<T>(IntPtr)
将字节转换为C#-struct,并且在DemoWithInterface
的情况下,附加Product
- 方法和vtable(或其他元数据)使结构实现IDemo
。