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

Why does LayoutKind.Sequential work differently if a struct contains a DateTime field?

如果结构包含日期时间字段,那么为什么layoutKind.Sequential的工作方式不同?

考虑以下代码(必须在启用"不安全"的情况下编译的控制台应用程序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
    }
}

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

结构地址=40F2CC第一个地址=40f2d4notfirst地址=40f2cc

注意,first的地址与结构的地址不同;但是notfirst的地址与结构的地址相同。

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

结构地址=15f2e0第一个地址=15f2e0notfirst地址=15f2e8

现在"first"与结构具有相同的地址。

我发现这种行为令人惊讶,因为使用了布局类型。顺序。有人能解释一下吗?在使用COM DATETIME类型的C/C++结构互操作时,这种行为是否有任何影响?

[编辑]注意:我已经验证了当您使用marshal.structureToptr()封送结构时,数据是以正确的顺序封送的,"First"字段是第一个。这似乎表明它可以与interop一起工作。神秘的是为什么内部布局会改变——当然,内部布局从未被指定,所以编译器可以做它喜欢的事情。

[edit2]从结构声明中删除了"unsafe"(它是我所做的一些测试的剩余部分)。

[edit3]此问题的原始来源是来自msdn c论坛:

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


Why does LayoutKind.Sequential work differently if a struct contains a DateTime field?

这与(令人惊讶的)事实有关,DateTime本身具有布局"auto"(链接到自己提出的问题)。此代码复制了您看到的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}

样品输出:

1
2
3
4
5
6
7
64-bit: True
Layout of OneField: Auto
Layout of Composite: Sequential
Size of Composite: 24
L: 48F050
M: 48F048
N: 48F058

如果我们从OneField中删除该属性,事情就会按预期进行。例子:

1
2
3
4
5
6
7
64-bit: True
Layout of OneField: Sequential
Layout of Composite: Sequential
Size of Composite: 24
L: 48F048
M: 48F050
N: 48F058

这些例子是用X64平台编译的(所以24的大小是8的3倍,这不足为奇),但是用X86我们也看到了相同的"无序"指针地址。

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

根据另一个线程中HansPassant的评论,当其中一个成员是Auto布局结构时,它不再试图保持其顺序。


更仔细地阅读布局规则的规范。仅当对象在非托管内存中公开时,布局规则才控制布局。这意味着编译器可以随意放置字段,直到实际导出对象为止。让我有点惊讶的是,对于FixedLayout,这甚至是正确的!

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

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

那里!不是很明显吗?

顺便说一下,所有这些在静态编译时都是可以识别的。实际上,编译器识别它是为了决定它可以忽略布局指令。在识别出它之后,编译器发出的警告似乎是正确的。实际上你没有做错什么,但是当你写了一些没有效果的东西时,别人告诉你是很有帮助的。

这里建议固定布局的各种注释通常都是很好的建议,但在这种情况下,这不一定有任何效果,因为包括datetime字段完全免除了编译器遵守布局的义务。更糟的是:编译器不需要遵守布局,但可以自由遵守布局。这意味着连续版本的clr在这方面可以自由地表现出不同的行为。

在我看来,布局的处理是CLI中的一个设计缺陷。当用户指定一个布局时,编译器不应该绕着它们跑。最好保持简单,让编译器按照命令进行操作。尤其是在布局方面。"我们都知道,聪明是一个四个字母的词。


回答我自己的问题(按建议):

问:"在使用COM DATETIME类型的C/C++结构互操作时,这种行为有什么影响吗?"

答:没有,因为使用编组时要尊重布局。(我用经验证明了这一点。)

问题"有人能解释吗?".

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


您正在检查托管结构中的地址。marshal属性对托管结构中的字段排列没有保证。

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

因此,管理结构的排列对本机结构的排列没有影响。只有属性会影响本机结构的排列。

如果使用marshal属性设置的字段以与本机数据相同的方式存储在托管数据中,那么marshal.structureToptr中就没有点,您只需通过字节复制数据即可。


几个因素

  • 双打如果对齐的话会快很多
  • 如果在打击中没有"漏洞",CPU缓存可能工作得更好。

因此,C编译器有一些未经记录的规则,用来尝试获得结构的"最佳"布局,这些规则可以考虑结构的总大小,和/或如果它包含另一个结构等。如果您需要知道结构的布局,那么您应该自己指定它,而不是让编译器决定。

然而,layoutKind.Sequential确实会停止编译器更改字段顺序。


如果你要用C/C++来互操作,我会一直使用StuttDebug。我将使用显式而不是顺序,并使用fieldoffset指定每个位置。此外,添加pack变量。

1
2
3
4
5
6
7
8
9
10
[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)。

包变量在C++代码中尤其重要,它可以编译在具有不同字大小的不同系统上。

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