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

Mat*_*son 21 c# datetime marshalling structlayout

如果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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

注意,First的地址与struct的地址不同; 然而,NotFirst的地址一样的结构的地址.

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

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

现在,"第一" 确实有相同的地址结构.

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

[编辑]注:我已经验证,当你使用Marshal.StructureToPtr()当元帅的结构,该数据按照正确的顺序编组,与"第一"字段为第一.这似乎表明它可以与互操作一起使用.谜团是内部布局发生变化的原因 - 当然,内部布局从未指定过,因此编译器可以做自己喜欢的事情.

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

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

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

Jep*_*sen 16

如果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;
}
Run Code Online (Sandbox Code Playgroud)

样本输出:

64-bit: True
Layout of OneField: Auto
Layout of Composite: Sequential
Size of Composite: 24
L: 48F050
M: 48F048
N: 48F058

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

64-bit: True
Layout of OneField: Sequential
Layout of Composite: Sequential
Size of Composite: 24
L: 48F048
M: 48F050
N: 48F058

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

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


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

  • 最后回答这个问题实际上是有道理的. (2认同)

Jon*_*iro 6

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

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

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

那里!不是那么明显!

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

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

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