我遇到了一个奇怪的场景,它在C#/ .NET中包含了包含数组的联合。请考虑以下程序:
namespace Marshal
{
class Program
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct InnerType
{
byte Foo;
//[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
//byte[] Bar;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct UnionType
{
[FieldOffset(0)]
InnerType UnionMember1;
[FieldOffset(0)]
[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
byte[] UnionMember2;
}
static void Main(string[] args)
{
Console.WriteLine(@"SizeOf UnionType: {0}", System.Runtime.InteropServices.Marshal.SizeOf(typeof(UnionType)));
}
}
}
如果您运行此程序,您将收到以下异常:
Could not load type 'UnionType' from assembly 'Marshal, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 0 that is incorrectly aligned or overlapped by a non-object field.
现在,如果您取消注释两个注释掉的行,程序将运行正常。我想知道为什么会这样。为什么在InnerType中添加额外的数组可以解决问题呢?顺便说一下,你制作阵列的大小并不重要。如果没有数组,UnionMember1和UnionMember2应该在大小上相互匹配。对于数组,它们的大小不匹配,但不会抛出任何异常。
更新 将InnerType更改为以下内容也会导致异常(此时在InnerType上):
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct InnerType
{
[FieldOffset(0)]
byte Foo;
[FieldOffset(1)]
[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
byte[] Bar;
}
在我看来,这应该等同于原始代码(LayoutKind.Sequential
),其中byte[] Bar
取消注释。
我不相信这里的问题与单词边界有关 - 我使用Pack = 1.相反,我认为它是异常的第二部分,“...它包含偏移处的对象字段0是......由非对象字段重叠。“ byte []是引用类型,而byte本身是值类型。我可以看到“byte Foo”最终会重叠“byte [] UnionMember2”。但是,这仍然无法解释为什么在原始代码中取消注释“byte [] bar”会使异常消失。
答案 0 :(得分:0)
我的假设是顺序布局被推翻,如本答案所述:LayoutKind.Sequential not followed when substruct has LayoutKind.Explicit。
请注意,pack设置为1并不能消除填充,即Bar
没有FieldOffset为1.它对齐到4个字节(在x32上),并且根据Marshal.OffsetOf()
它应该按预期为4。
但是,.NET运行时实际上可能会将引用类型Bar
放在托管内存中的字节Foo
之前,在这种情况下,它会在与UnionMember2
的联合中正确重叠。
有趣的是,使用Foo
int和float会发生相同的情况,但是使用long和double会再次出现异常。看起来它在大小上对字段进行排序,但如果大小相等则首先放置引用类型。
当我切换到x64时,它也与long Foo
一起使用,这将支持这一理论。最后,我打开了一个内存窗口(Debug-> Windows-> Memory)并输入位置&instance.UnionMember1.Foo
并向上滚动一点以显示Foo
之前的字节。然后使用立即窗口在Foo和Bar中设置一个值,证明Bar
为0且Foo
为4.(将var instance = new UnionType()
添加到Main
)
请注意,虽然这可能不是您的意图,但Byte[]
仅被视为参考类型。您可以将其替换为object
。您可以改为使用fixed byte Bar[1]
,具体取决于您的目标。