为什么通用类型不能有显式布局?

13

如果尝试使用[StructLayout(LayoutKind.Explicit)]属性创建通用结构体,在运行时使用该结构体将会引发异常:

System.TypeLoadException: Could not load type 'foo' from assembly 'bar' because generic types cannot have explicit layout.

我一直找不到任何证据证明这种限制的存在。 Type.IsExplicitLayout文档强烈暗示它是允许和支持的。有人知道为什么不允许吗?我想不出泛型类型会使验证变得更加困难的理由。这似乎只是一个他们没有费力去实现的边缘情况。

这里是显示显式泛型布局将是有用的示例

public struct TaggedUnion<T1,T2>
{
    public TaggedUnion(T1 value) { _union=new _Union{Type1=value}; _id=1; }
    public TaggedUnion(T2 value) { _union=new _Union{Type2=value}; _id=2; }

    public T1 Type1 { get{ if(_id!=1)_TypeError(1); return _union.Type1; } set{ _union.Type1=value; _id=1; } }
    public T2 Type2 { get{ if(_id!=2)_TypeError(2); return _union.Type2; } set{ _union.Type2=value; _id=2; } }

    public static explicit operator T1(TaggedUnion<T1,T2> value) { return value.Type1; }
    public static explicit operator T2(TaggedUnion<T1,T2> value) { return value.Type2; }
    public static implicit operator TaggedUnion<T1,T2>(T1 value) { return new TaggedUnion<T1,T2>(value); }
    public static implicit operator TaggedUnion<T1,T2>(T2 value) { return new TaggedUnion<T1,T2>(value); }

    public byte Tag {get{ return _id; }}
    public Type GetUnionType() {switch(_id){ case 1:return typeof(T1);  case 2:return typeof(T2);  default:return typeof(void); }}

    _Union _union;
    byte _id;
    void _TypeError(byte id) { throw new InvalidCastException(/* todo */); }

    [StructLayout(LayoutKind.Explicit)]
    struct _Union
    {
        [FieldOffset(0)] public T1 Type1;
        [FieldOffset(0)] public T2 Type2;
    }
}

用法:

TaggedUnion<int, double> foo = 1;
Debug.Assert(foo.GetUnionType() == typeof(int));
foo = 1.0;
Debug.Assert(foo.GetUnionType() == typeof(double));
double bar = (double) foo;

编辑:

为了明确,即使结构体不是泛型的,布局也没有在编译时进行验证。CLR会在运行时检测引用重叠和x64差异:http://pastebin.com/4RZ6dZ3S 我想知道为什么泛型会受到限制,当检查也是在运行时进行。

3个回答

10

在ECMA 335 (CLI)的第II部分第II.10.1.2节中有明确规定:

explicit: 字段的布局是显式提供的(§II.10.7)。 然而,泛型类型不应具有显式布局。

可以想象,这可能会很尴尬 - 因为类型参数的大小取决于类型参数本身,因此您可能会得到一些非常奇怪的效果...例如,引用字段不能与内置值类型或其他引用重叠,当涉及未知大小时难以保证。(我还没有研究32位与64位引用的处理方式,它们存在着类似但略有不同的问题...)

我怀疑规范可能已经写得更详细一些以施加一些限制 - 但对所有泛型类型实行简单的全面限制要简单得多。


1
常规结构体已经可以在运行时变化。(参见:IntPtr) 即使它不是泛型,验证引用类型和值类型之间没有重叠的限制已经由CLR独家处理:http://pastebin.com/4RZ6dZ3S 如果规则被取消,那么泛型需要做出什么“更详细的限制”? - DBN
@IllidanS4 是的,那就是我所问的。 - DBN
@DBN:更详细的限制将是一个更复杂的有效或无效设置。例如,假设T本身是具有显式布局的值类型...或者假设有两个类型参数,两个参数都具有显式布局,并且它们在泛型类型的布局中重叠。我认为这变得非常复杂,以至于最好禁止它。我远非这个特定领域的专家,但我知道有很多地方会排除可能的功能,以使全局禁令更简单。 - Jon Skeet
@DBN:我认为这更多是由于泛型而变得显著恶化。最终,我们现在只是在猜测 - 我提供了限制存在的证据,但除此之外,至少在我的部分,这只是猜测。 - Jon Skeet
@DBN:.NET运行时要求/假设如果某个类型对于满足其约束条件的某些泛型类型参数组合是合法的,则该类型必须/将在所有这样的组合中都是合法的。这确保了可以通过独立确定Foo<>的有效性(不考虑T),并确定T是否遵守Foo<T>指定的任何约束来评估类型Foo<T>的有效性。 - supercat
显示剩余6条评论

6
问题的根源在于泛型和可验证性,以及基于类型约束的设计。我们不能将引用(指针)与值类型重叠是一个隐含的、多参数约束规则。所以,我们知道CLR足够聪明,在非泛型情况下可以验证这一点……那么为什么不在泛型中呢?听起来很有吸引力。
正确的泛型类型定义是指对于任何现有类型(在约束范围内)和未来定义的任何类型都能工作的可验证类型定义。编译器会自行验证开放式泛型类型定义,考虑您指定的任何类型约束来缩小可能的类型参数。
在没有更具体的类型约束的情况下,对于> , T和U代表所有可能值和引用类型的并集,以及这些类型(即基本的System.Object)共同的接口。如果我们想使T或U更加具体,我们可以添加主要和次要类型约束。在C#最新版本中,我们可以约束最具体的类或接口。结构体或原始类型限制不支持。
我们目前无法说:
1.只有struct或value type 2.如果T是密封类型,则T
public struct TaggedUnion<T1, T2>
    where T1 : SealedThing   // illegal

所以我们无法定义一个通用类型,可验证它永远不会违反 TU 中所有类型的重叠规则。即使我们可以通过结构进行限制,你仍然可以派生带有引用字段的结构,使得将来的某个类型 T<,> 不正确。
因此,我们真正在问的是“为什么泛型类型不允许基于类代码的隐式类型约束?”;显式布局是内部实现细节,对哪些 T1T2 的组合是合法的施加了限制。在我看来,这与依赖于类型约束的设计不一致。它违反了泛型类型系统的清晰契约。那么,如果我们打算打破它,为什么一开始要在设计中引入类型约束系统呢?我们可以把它扔掉并用异常来替换它。
当前状态如下:
1. 类型约束是开放通用类型的可见元数据 2. 针对开放定义 F<,> 对通用类型 Foo<T,U> 进行验证一次。对于每个绑定类型实例 Foo<t1,u1> ,都会检查 t1 和 u1 是否符合约束的类型正确性。不需要重新验证 Foo<t1,u1> 的类和方法代码。
这个结论仅表示“据我所知”。
从技术上讲,没有硬性技术原因阻止对每个泛型类型实例进行语义分析的正确性(C++ 是这方面的证据),但这似乎会破坏已有的设计。
简而言之,如果不打破或补充现有的类型约束设计,就没有办法验证它。
也许,结合适当的新类型约束,我们将来可能会看到这一点。

顺便说一句,通过从静态构造函数中抛出异常也可以实现同样类型的“强制限制”。(鉴于泛型约束非常弱,我不会惊讶于在实践中看到这样的做法。)但我猜想他们限制这个的理由可能就是这个。 - DBN
@DBN - 我猜这确实是理由,否则人们可以认为类型约束系统可以被抛弃而使用异常。我也同意你的看法,它们功能不足。但好的一面是,与C++相比,C# / CTS实现更容易编写。作为一个编译器编写者,我可以有机会在没有帮助的情况下实现自己的语言与CTS泛型。对于C++模板,最好有一个团队与你合作。我必须承认,C#是一个相对清晰的设计。 - codenheim
我没有看到任何好的方法来向类型系统添加任何其他种类的通用约束,因为它假定了一组非继承性的非常固定的约束条件 [new、struct 和 class]。虽然为例如“可复制为值”这样的约束可能会有所帮助(使得定义结构体成为可能),但是目前没有通用约束要求它,因此没有现有的通用类型能够满足它。可能需要的是一个反约束,这样不能作为值进行复制的结构体只能被传递给通用参数... - supercat
...其中在元数据中有一个NeedNotBeCopyableAsValue的“约束条件”[所有类型都将满足该约束条件,因为其目的是为了使默认情况下应用的约束条件失效]。 - supercat

3
.NET框架的设计对于泛型类型做出了一些假设,这些假设使得显式布局结构无法拥有任何字段,它们的大小可能会根据泛型类型参数而变化。其中最基本的是:
- 如果存在任何与泛型类型定义相匹配的类型参数组合,则可以假定其对于满足其指定约束条件的所有参数组合都有效。
因此,对于非约束为 class 的传入泛型类型,除了最后一个字段之外,.NET 不可能允许显式布局结构使用它作为任何字段的类型。而使用传入泛型类型作为参数的泛型值类型必须与传入类型本身受到相同的约束。
我认为,如果不允许任何内容覆盖该类型或以其作为类型参数的任何值类型的任何字段,并且如果该字段未知为引用类型,则显式布局结构具有类约束的泛型类型参数应该没有特别的问题,但该字段必须是结构中的最后一个元素。
另一方面,大多数“安全”的用例可以通过使用包含泛型类型的非显式布局结构来更好地处理,并在其中嵌套一个或多个显式布局结构。这种方法在语义上可以完成与显式布局结构相同的所有操作。唯一的麻烦是在访问嵌套成员时必须在源代码中添加额外的间接层,而其解决方法不是允许泛型显式布局结构,而是提供一种方式,通过该方式包含另一个结构的结构可以为内部结构成员创建别名。
例如:
[StructLayout(LayoutKind.Explicit)]
public struct _UnionLongAnd2Ints
{
    [FieldOffset(0)] public int LowerWord;
    [FieldOffset(4)] public int UpperWord;
    [FieldOffset(0)] public long Value;
}
public struct LongTwoIntsUnionAndSomethingElse<T>
{
  UnionLongAnd2Ints UnionPart;
  T OtherPart;
}

这里的通用结构包含一个64位值,它覆盖在两个32位值上,但应该是可验证的,因为显式布局部分没有任何通用性。


我不认为我理解你所说的嵌套显式结构。它们会包含什么?这与我的示例中的嵌套显式布局结构有什么区别吗?(_Union) - DBN
@DBN:请参考上面的示例。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接