如果struct包含DateTime字段,为什么LayoutKind.Sequential的工作方式不同?

如果struct包含DateTime字段,为什么LayoutKind.Sequential的工作方式不同?

请考虑以下代码(必须使用“unsafe”编译的控制台应用程序):

using System; using System.Runtime.InteropServices; namespace ConsoleApplication3 { static class Program { static void Main() { Inner test = new Inner(); unsafe { Console.WriteLine("Address of struct = " + ((int)&test).ToString("X")); Console.WriteLine("Address of First = " + ((int)&test.First).ToString("X")); Console.WriteLine("Address of NotFirst = " + ((int)&test.NotFirst).ToString("X")); } } } [StructLayout(LayoutKind.Sequential)] public struct Inner { public byte First; public double NotFirst; public DateTime WTF; } } 

现在如果我运行上面的代码,我得到类似于以下的输出:

struct = 40F2CC的地址
First = 40F2D4的地址
NotFirst的地址= 40F2CC

请注意,First的地址与struct的地址不同; 但是,NotFirst的地址与struct的地址相同。

现在注释掉结构中的“DateTime WTF”字段,然后再次运行它。 这一次,我得到的输出类似于:

struct = 15F2E0的地址
First = 15F2E0的地址
NotFirst的地址= 15F2E8

现在“First” 的确具有与struct相同的地址。

鉴于使用LayoutKind.Sequential,我发现这种行为令人惊讶。 任何人都可以提供解释吗? 与使用Com DATETIME类型的C / C ++结构进行互操作时,此行为是否有任何后果?

[编辑]注意:我已经validation当您使用Marshal.StructureToPtr()来编组结构时,数据将以正确的顺序编组,首先是“First”字段。 这似乎表明它可以与互操作一起使用。 谜团是内部布局发生变化的原因 – 当然,内部布局从未指定过,因此编译器可以做自己喜欢的事情。

[EDIT2]从结构声明中删除了“unsafe”(这是我正在进行的一些测试中的剩余部分)。

[EDIT3]此问题的原始来源是来自MSDN C#论坛:

http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/fb84bf1d-d9b3-4e91-823e-988257504b30

如果struct包含DateTime字段,为什么LayoutKind.Sequential的工作方式不同?

它与(令人惊讶的)事实有关,即DateTime本身具有布局“自动”(由我自己链接到SO问题) 。 此代码重现您所看到的行为:

 static class Program { static unsafe void Main() { Console.WriteLine("64-bit: {0}", Environment.Is64BitProcess); Console.WriteLine("Layout of OneField: {0}", typeof(OneField).StructLayoutAttribute.Value); Console.WriteLine("Layout of Composite: {0}", typeof(Composite).StructLayoutAttribute.Value); Console.WriteLine("Size of Composite: {0}", sizeof(Composite)); var local = default(Composite); Console.WriteLine("L: {0:X}", (long)(&(local.L))); Console.WriteLine("M: {0:X}", (long)(&(local.M))); Console.WriteLine("N: {0:X}", (long)(&(local.N))); } } [StructLayout(LayoutKind.Auto)] // also try removing this attribute struct OneField { public long X; } struct Composite // has layout Sequential { public byte L; public double M; public OneField N; } 

样本输出:

 64位:是的
 OneField的布局:自动
复合布局:顺序
复合材料的尺寸:24
 L:48F050
 M:48F048
 N:48F058

如果我们从OneField删除该属性,则事情会按预期运行。 例:

 64位:是的
 OneField的布局:顺序
复合布局:顺序
复合材料的尺寸:24
 L:48F048
 M:48F050
 N:48F058

这些示例是使用x64平台编译的(因此大小为24,三次八,并不令人惊讶),但是对于x86,我们看到相同的“无序”指针地址。

所以我想我可以得出结论, OneField的布局(在你的例子中是OneField )会影响包含OneField成员的struct的布局,即使该复合struct本身具有Sequential布局。 我不确定这是否有问题(甚至是必需的)。


根据Hans Passant在另一个post中的评论,当其中一个成员是Auto布局结构时, 它不再尝试保持顺序

请仔细阅读布局规则的规范。 布局规则仅在对象在非托管内存中公开时控制布局 。 这意味着编译器可以自由地放置它想要的字段,直到实际导出对象。 令我惊讶的是,这对于FixedLayout来说确实如此!

Ian Ringrose关于编译器效率问题是正确的,这确实考虑了这里选择的最终布局,但它与编译器忽略布局规范的原因无关。

有几个人指出DateTime有自动布局。 这是你惊喜的最终来源,但原因有点模糊。 Auto布局的文档说“使用[Auto]布局定义的对象不能在托管代码之外公开。尝试这样做会产生exception。” 另请注意,DateTime是值类型。 通过将具有自动布局的值类型合并到您的结构中,您无意中承诺永远不会将包含的结构暴露给非托管代码(因为这样做会暴露DateTime,这会产生exception)。 由于布局规则仅管理非托管内存中的对象,并且您的对象永远不会暴露给非托管内存,因此编译器不会对其布局选择进行约束,并且可以随意执行任何操作。 在这种情况下,它将恢复为自动布局策略,以实现更好的结构打包和对齐。

那里! 不是那么明显!

顺便说一句,所有这些在静态编译时都是可识别的。 事实上,编译器正在识别它,以便决定它可以忽略你的布局指令。 已经认识到它,编译器的警告似乎是有序的。 你实际上并没有做错任何事情,但是当你写下一些没有效果的东西时,告诉它是有帮助的。

这里推荐固定布局的各种注释通常都是很好的建议,但在这种情况下不一定会产生任何影响,因为包含DateTime字段可以免除编译器对布局的影响。 更糟糕的是:编译器不需要遵循布局,但可以自由地遵循布局。 这意味着CLR的连续版本可以自由地表现出不同的行为。

在我看来,布局的处理是CLI中的一个设计缺陷。 当用户指定布局时,编译器不应该围绕它们进行律师处理。 最好保持简单,让编译器按照它所说的去做。 特别是在布局方面。 众所周知,“聪明”是一个四字母的单词。

回答我自己的问题(如建议的那样):

问题:“当使用使用Com DATETIME类型的C / C ++结构进行互操作时,此行为是否有任何后果?”

答:不,因为使用编组时会考虑布局。 (我凭经validation实了这一点。)

问题“任何人都可以提供解释吗?”。

答:我仍然不确定这一点,但由于没有定义结构的内部表示,编译器可以做它喜欢的事情。

几个因素

  • 如果对齐,双打会快得多
  • 如果打击中没有“漏洞”,CPU缓存可能会更好

因此,C#编译器有一些未记录的规则,用于尝试获取结构的“ 最佳 ”布局,这些规则可能会考虑结构的总大小,和/或它是否包含另一个结构等。 如果需要知道结构的布局然后你应该自己指定它而不是让编译器决定。

但是,LayoutKind.Sequential会阻止编译器更改字段的顺序。

您正在检查托管结构中的地址。 Marshal属性无法保证托管结构中的字段排列。

它正确地编组到本机结构中的原因是因为使用由编组值设置的属性将数据复制到本机内存中。

因此,管理结构的安排对原生结构的排列没有影响。 只有属性才会影响原生结构的排列。

如果使用marshal属性设置的字段以与本机数据相同的方式存储在托管数据中,那么Marshal.StructureToPtr中没有任何意义,您只需对数据进行字节复制即可。

如果您要与C / C ++互操作,我将始终具体使用StructLayout。 我将使用Explicit,而不是Sequential,并使用FieldOffset指定每个位置。 另外,添加您的Pack变量。

 [StructLayout(LayoutKind.Explicit, Pack=1, CharSet=CharSet.Unicode)] public struct Inner { [FieldOffset(0)] public byte First; [FieldOffset(1)] public double NotFirst; [FieldOffset(9)] public DateTime WTF; } 

听起来无论如何,DateTime都不能被编组,只能用于字符串(bingle Marshal DateTime)。

Pack变量在C ++代码中尤为重要,C ++代码可能在具有不同字长的不同系统上编译。

我也会忽略使用不安全代码时可以看到的地址。 只要编组正确,编译器的作用并不重要。