当子结构具有LayoutKind.Explicit时,不遵循LayoutKind.Sequential

运行此代码时:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.InteropServices; namespace StructLayoutTest { class Program { unsafe static void Main() { Console.WriteLine(IntPtr.Size); Console.WriteLine(); Sequential s = new Sequential(); sA = 2; sB = 3; s.Bool = true; s.Long = 6; sCInt32a = 4; sCInt32b = 5; int* ptr = (int*)&s; Console.WriteLine(ptr[0]); Console.WriteLine(ptr[1]); Console.WriteLine(ptr[2]); Console.WriteLine(ptr[3]); Console.WriteLine(ptr[4]); Console.WriteLine(ptr[5]); Console.WriteLine(ptr[6]); Console.WriteLine(ptr[7]); //NB! Console.WriteLine("Press any key"); Console.ReadKey(); } [StructLayout(LayoutKind.Explicit)] struct Explicit { [FieldOffset(0)] public int Int32a; [FieldOffset(4)] public int Int32b; } [StructLayout(LayoutKind.Sequential, Pack = 4)] struct Sequential { public int A; public int B; public bool Bool; public long Long; public Explicit C; } } } 

我希望在x86和x64上输出两个:
4或8 (取决于x86或x64)

2
3
1
6
0
4

垃圾

我得到的是x86:
4

6
0
2
3
1
4

垃圾

我得到的是x64:
8

6
0
2
3
1
0
4

更多:
– 当我删除LayoutKind.Explicit和FieldOffset属性时,问题就消失了。
– 当我删除Bool字段时,问题就消失了。
– 当我删除Long字段时,问题就消失了。
– 请注意,在x64上似乎也忽略了Pack = 4属性参数?

这适用于.Net3.5和.Net4.0

我的问题:我错过了什么? 或者这是一个错误?
我发现了一个类似的问题:
如果struct包含DateTime字段,为什么LayoutKind.Sequential的工作方式不同?
但在我的情况下,即使子结构的属性发生更改,布局也会更改,而不会对数据类型进行任何更改。 所以它看起来不像是一种优化。 除此之外,我想指出另一个问题仍然没有答案。
在另一个问题中,他们提到使用编组时布局得到尊重。 我自己没有测试过,但我想知道为什么不安全代码不考虑布局,因为所有相关属性似乎都已到位? 除非完成编组,否则文档是否提到忽略这些属性的地方? 为什么?
考虑到这一点,我甚至可以期望LayoutKind.Explicit能够可靠地处理不安全的代码吗?
此外,文档提到了保持结构符合预期布局的动机:

为减少与Auto值相关的布局相关问题,C#,Visual Basic和C ++编译器为值类型指定顺序布局。

但这个动机显然不适用于不安全的代码?

从用于LayoutKind枚举的MSDN Library文章:

根据StructLayoutAttribute.Pack字段的设置,显式控制对象在非托管内存中的每个成员的精确位置。 每个成员必须使用FieldOffsetAttribute来指示该类型中该字段的位置。

相关的短语突出显示,这个程序没有发生,指针仍然是非常多的解引用托管内存。

是的,您看到的内容与结构包含DateTime类型的成员时发生的情况相同,DateTime是一种应用了[StructLayout(LayoutKind.Auto)]的类型。 确定布局的CLR中的字段编组器代码也努力为受管理的结构提供LayoutKind.Sequential。 但如果遇到与此目标冲突的任何成员,它将很快放弃而不会尖叫。 一个本身不是顺序的结构就足够了。 您可以在SSCLI20源代码 src / clr / vm / fieldmarshaler.cpp中查看此操作,搜索fDisqualifyFromManagedSequential

这将使其切换到自动布局,与应用于类的布局规则相同。 它重新排列字段以最小化成员之间的填充。 净效应是所需的内存量更小。 在“Bool”成员之后有7个字节的填充,未使用的空间使“Long”成员对齐到8的倍数的地址。非常浪费当然,它修复了通过使long成为布局中的第一个成员。

因此,而不是使用/ * offset – size * / annotated的显式布局:

  public int A; /* 0 - 4 */ public int B; /* 4 - 4 */ public bool Bool; /* 8 - 1 */ // padding /* 9 - 7 */ public long Long; /* 16 - 8 */ public Explicit C; /* 24 - 8 */ /* Total: 32 */ 

它提出:

  public long Long; /* 0 - 8 */ public int A; /* 8 - 4 */ public int B; /* 12 - 4 */ public bool Bool; /* 16 - 1 */ // padding /* 17 - 3 */ public Explicit C; /* 20 - 8 */ /* Total: 28 */ 

轻松保存4个字节的内存。 64位布局需要额外的填充以确保长整数存储在数组中时仍然对齐。 这些都是高度无证的,可能会发生变化,请务必永远不要依赖托管内存布局。 只有Marshal.StructureToPtr()可以给你一个保证。