关于本身是结构的字段的LayoutKind.explicit的c#:. NET行为

.NET behaviour of LayoutKind.explicit for a field which is itself a struct

我尝试使用[StructLayout(LayoutKind.Explicit)]构建一个结构(SA),该结构的字段是另一个struct(SB)。

首先:我很惊讶我被允许声明没有[StructLayout(LayoutKind.Explicit)]的其他结构,而在SA中,所有字段都必须具有[FieldOffset(0)],否则编译器会大喊大叫。 这没有多大意义。

  • 这是编译器警告/错误中的漏洞吗?

第二:似乎SB中的所有引用(object)字段都移到了SB的前面。

  • 这种行为在任何地方都有描述吗?
  • 是否依赖于实现?
  • 是否在任何依赖于实现的地方进行了定义? :)

注意:我不打算在生产代码中使用它。 我主要出于好奇而问这个问题。

实验性

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// No object fields in SB
// Gives the following layout (deduced from experimentation with the C# debugger):

// | f0 | f4 and i | f8 and j | f12 and k | f16 |

[StructLayout(LayoutKind.Explicit)]
struct SA {
    [FieldOffset(0)] int f0;
    [FieldOffset(4)] SB sb;
    [FieldOffset(4)] int f4;
    [FieldOffset(8)] int f8;
    [FieldOffset(12)] int f12;
    [FieldOffset(16)] int f16;
}
struct SB { int i; int j; int k; }

// One object field in SB
// Gives the following layout:

// | f0 | f4 and o1 | f8 and i | f12 and j | f16 and k |

// If I add an `object` field after `j` in `SB`, i *have* to convert
// `f4` to `object`, otherwise I get a `TypeLoadException`.
// No other field will do.

[StructLayout(LayoutKind.Explicit)]
struct SA {
    [FieldOffset(0)] int f0;
    [FieldOffset(4)] SB sb;
    [FieldOffset(4)] object f4;
    [FieldOffset(8)] int f8;
    [FieldOffset(12)] int f12;
    [FieldOffset(16)] int f16;
}
struct SB { int i; int j; object o1; int k; }

// Two `object` fields in `SB`
// Gives the following layout:

// | f0 | f4 and o1 | f8 and o2 | f12 and i | f16 and j | k |

// If I add another `object` field after the first one in `SB`, i *have* to convert
// `f8` to `object`, otherwise I get a `TypeLoadException`.
// No other field will do.

[StructLayout(LayoutKind.Explicit)]
struct SA {
    [FieldOffset(0)] int f0;
    [FieldOffset(4)] SB sb;
    [FieldOffset(4)] object f4;
    [FieldOffset(8)] object f8;
    [FieldOffset(12)] int f12;
    [FieldOffset(16)] int f16;
}
struct SB { int i; int j; object o1; object o2; int k; }


Is this a loophole in the compiler's warnings/errors ?

不,没有错。允许字段重叠,这就是为什么LayoutKind.Explicit首先存在的原因。它允许在非托管代码中声明联合的等效项,否则C#不支持。您不能突然停止在结构声明中使用[FieldOffset],编译器坚持要求您在结构的所有成员上使用它。从技术上来讲不是必需的,而是一个避免错误假设的简单要求。

it seems that all reference (object) fields in SB are moved

是的,这很正常。 CLR以无证件和无法发现的方式布置对象。它使用的确切规则未记录在案,并可能随时更改。对于不同的抖动也不会重复。直到将对象编组,Marshal.StructureToPtr()调用或通过pinvoke编组器隐式布局后,布局才变得可预测。这是唯一的确切布局问题。我在这个答案中写了这种行为的基本原理。


第一个问题的答案是否定的,编译器的错误报告中没有漏洞或错误。如果您开始进行显式布局,则编译器将假设您知道自己在做什么(在限制之内-参见下文)。您告诉它在一个结构上覆盖另一个结构。编译器不(也不应该)不在乎您要覆盖的结构是否也未明确布局。

如果编译器确实在意,那么您将无法覆盖任何未明确布局的类型,这意味着在一般情况下您无法进行并集。例如,考虑尝试覆盖DateTimelong

1
2
3
4
5
6
7
8
9
10
[StructLayout(LayoutKind.Explicit)]
struct MyUnion
{
    [FieldOffset(0)]
    public bool IsDate;
    [FieldOffset(1)]
    public DateTime dt;
    [FieldOffset(1)]
    public long counter;
}

除非明确布置DateTime,否则将无法编译。可能不是您想要的。

至于将引用类型放入显式布局的结构中,您的结果将……可能不是您所期望的。考虑一下这个简单的例子:

1
2
3
4
5
6
7
struct MyUnion
{
    [FieldOffset(0)]
    public object o1;
    [FieldOffset(0)]
    public SomeRefType o2;
}

这在很大程度上违反了类型安全性。如果编译(很可能会编译),当您尝试使用它时,它将死于TypeLoadException。

编译器将在可能的情况下防止您违反类型安全。我不知道编译器是否知道如何处理这些属性并布局结构,还是只是通过生成的MSIL将布局信息传递给运行时。考虑到您的第二个示例,可能是后者。在该示例中,编译器允许使用特定的布局,但运行时使用TypeLoadException进行轰炸。

在[structlayout.explicit引用类型]上进行的Google搜索显示了一些有趣的讨论。例如,请参阅以显式struct?相互覆盖多个CLR参考字段。