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

时间:2010-11-09 10:14:36

标签: 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;
    }
}

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

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()来编组结构时,数据 以正确的顺序编组,“First”字段为第一个。这似乎表明它可以与互操作一起使用。谜团是内部布局发生变化的原因 - 当然,内部布局从未指定过,因此编译器可以做自己喜欢的事情。

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

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

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

6 个答案:

答案 0 :(得分:15)

  

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

(surprising) fact that DateTime itself has layout "Auto" (link to SO question by myself)有关。此代码重现您所看到的行为:

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-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(在您的示例中为resp。DateTime)的布局对包含OneField成员的结构的布局有影响,即使该复合struct本身有布局Sequential。我不确定这是否有问题(甚至是必需的)。


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

答案 1 :(得分:6)

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

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

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

有!没那么明显!

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

这里推荐固定布局的各种评论通常是很好的建议,但在这种情况下,它不一定会产生任何影响,因为包括DateTime字段可以免除编译器对布局的影响。更糟糕的是:编译器不是必需来表示布局,但它是 free 来表示布局。这意味着CLR的后续版本可以在此处自由表现。

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

答案 2 :(得分:3)

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

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

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

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

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

答案 3 :(得分:2)

一些因素

  • 如果对齐,则双打速度要快得多
  • 如果被击中的“漏洞”
  • ,CPU缓存可能会更好

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

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

答案 4 :(得分:2)

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

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

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

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

答案 5 :(得分:1)

如果您要与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 ++代码可能在具有不同字长的不同系统上编译。

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