通过动态访问generics类型的成员时的StackOverflowException:.NET / C#framework bug?

在一个程序中,我使用dynamic关键字来调用最佳匹配方法。 但是,我发现在某些情况下框架会因StackOverflowException崩溃。

我尽可能地尝试简化我的代码,同时仍然能够重新产生这个问题。

 class Program { static void Main(string[] args) { var obj = new SetTree(); var dyn = (dynamic)obj; Program.Print(dyn); // throws StackOverflowException!! // Note: this works just fine for 'everything else' but my SetTree } static void Print(object obj) { Console.WriteLine("object"); } static void Print(ISortedSet obj) { Console.WriteLine("set"); } } 

如果ISortedSet实例实现ISortedSet接口并打印“对象”,那么该程序通常会打印“set”。 但是,通过以下声明,将抛出StackOverflowException (如上面的注释中所述)。

 interface ISortedSet { } sealed class SetTree : BalancedTree<SetTreeNode>, ISortedSet {} abstract class BalancedTree where TNode : TreeNode { } abstract class SetTreeNode : KeyTreeNode<SetTreeNode, TKey> { } abstract class KeyTreeNode : TreeNode where TNode : KeyTreeNode { } abstract class TreeNode where TNode : TreeNode { } 

无论这是否是一个错误,抛出StackOverflowException是非常麻烦的,因为我们无法捕获它,并且几乎无法事先确定是否会抛出exception(从而终止进程!)。

有人可以解释一下发生了什么吗? 这是框架中的错误吗?

调试并切换到“反汇编模式”时,我看到了这个:

拆卸

在该位置注册转储: 注册转储

 EAX = 02B811B4 EBX = 0641EA5C ECX = 02C3B0EC EDX = 02C3A504 ESI = 02C2564C EDI = 0641E9AC EIP = 011027B9 ESP = 0641E91C EBP = 0641E9B8 EFL = 00000202 

这并不能告诉我更多的指标,这确实必须是框架中的某种错误。

我已经提交了有关Microsoft Connect的错误报告,但我很想知道这里发生了什么。 我的class级声明在某种程度上是不受支持的吗?

不知道为什么会发生这种情况会让我担心我们使用dynamic关键字的其他地方。 我完全不相信吗?

有人可以解释一下发生了什么吗? 这是框架中的错误吗?

是。

问题在于generics类型被解析为其特定的具体用途。

好吧,让我们从一些明显的东西开始,以便建立编译器出错的地方。 如您所知,使用List之类的编译器(无论是动态编译器,还是自C#2引入generics以来的任何静态编译器)都必须采用List<>类型和int类型并组合有关两者的信息生成List类型的那些。

现在,考虑一下:

 public class Base { } public class Derived : Base { } Derived l = new Derived(); 

在这里你可以看到,在Derived类型的相同工作中,编译器必须填充三个插槽:

  1. Derived<>上定义的T ,它被填充为long
  2. Base<,>上定义的T Base<,>用在Derived<>上定义的T填充,其中填充了long
  3. Base<,>上定义的Uint填充。

当您考虑嵌套类,长inheritance链,从其他generics类型派生的generics类型以及添加更多通用参数等时,您可以看到有很多不同的排列要覆盖。 如果您从Derived并且必须回答“该类的基本类型是什么?”的问题。 (显然编译器需要考虑很多)然后所有这些都必须解决。

动态编译器基于前Roslyn静态编译器,它基于之前的编译器,它实际上是用C ++而不是C#编写的(还有很多动态编译器,当它在C#中时,C ++的气味)。 可以认为在终点(可执行的代码)中比在起点上更相似; 一组必须为静态编译器解析的文本,以了解涉及哪些类型和操作与动态编译器相关,从已存在的类型和对象和标志表示的操作开始。

他们都需要知道的一件事是,如果一个类型被多次提到,那就是它是相同的类型(毕竟这几乎是类型意味着最基本的定义)。 如果我们编译new List((int)x)显然不会起作用,除非它知道int意味着两次同样的事情。 他们还需要避免咀嚼RAM的演出。

这两个问题都可以通过散列方法或类似flyweight的方法来解决。 当它构造表示特定类型的对象时,它首先看到它是否已经构造了该类型,并且只在必要时构造一个新类型。 这也有助于正确构建层次结构中的许多关系,但显然不是您问题中的特定情况。

对于大多数类型(除了一些特殊情况,如指针,引用,数组,nullables [虽然该exception有例外],键入参数…好吧,实际上有很多例外)状态主要是三件事:

  1. 表示没有特定类型参数的类型的符号(这是非generics类型的表示的总和),但它确实包含generics定义的类型参数(对于Dictionary它具有TKeyDictionary TValue Dictionary )。
  2. 直接在类型上的参数类型集(无论是开放类型的ListT ,构造类型的ListList ,还是例如Dictionary relative的混合一些定义T )的通用类型或方法。
  3. 直接在类型上的类型参数集(如上所述)或嵌套在外部类型上的类型参数集。

好的,到目前为止,这么好。 如果它需要对List.Enumerator做一些事情,它首先要么在商店中找到List符号,要么在新的时候添加它,然后在商店中找到List.Enumerator符号,或者如果new,然后在商店中查找intint作为一种非常常见的类型预加载),最后在商店中找到将List.Enumeratorint结合的类型,或者如果是new则添加它。 我们现在有唯一的 List.Enumerator类型对象。

导致您的错误的问题出现在最后一步的末尾。 考虑我们上面所说的在创建类型的具体实现时必须将类型分配给基类型。 具体generics类型的基类型是具体类型,可能本身就是具体generics类型,但我们这里的信息是generics类型和一些类型参数:我们不知道具体generics类型是什么。

查找基类型的方法是延迟加载的,但调用不知道要使用的类型参数的符号。

使用的解决方案是根据具体基本类型临时定义该符号的基本类型,调用延迟加载基本类型方法,然后再将其设置回来。

我不知道为什么在创建后立即调用它时会出现延迟加载的内容。 在猜测中,我会说它在静态编译方面更有意义,因此以这种方式移植而不是从头开始重写机制(在大多数情况下这将是一种更冒险的方法)。

这非常有效,即使是非常复杂的层次结构。 但是,如果某个层次结构在类型参数方面都是循环的, 并且在到达非generics类型(例如object )之前有多个步骤(因此修复也必须在基类型上进行递归)然后它无法在制作过程中找到它的类型(记住关于存储类型对象的位),因为它已被临时更改以使修复工作正常,并且必须再次进行修改。 再一次,直到你遇到StackOverflowException

来自Adam Maras的回答:

这让我相信(不了解运行时绑定程序的内部结构)它主动检查递归约束,但只有一个级别。

这几乎是相反的,因为问题是主动设置基类,以防止它意识到它已经拥有它需要的类型。 我想我今天设法修复了它,但是如果有人看到我错过了该修复的一些问题还有待观察(对框架做出贡献的好处是他们有高标准的代码审查,但这当然意味着我可以不确定一个贡献会被接受,直到它进入。

我创建了一个更短,更加重要的SSCCE来说明问题:

 class Program { static void Main() { dynamic obj = new Third(); Print(obj); // causes stack overflow } static void Print(object obj) { } } class First where T : First { } class Second : First where T : First { } class Third : Second> { } 

查看调用堆栈,它似乎在C#运行时绑定程序中的两对符号之间反弹:

 Microsoft.CSharp.RuntimeBinder.SymbolTable.LoadSymbolsFromType( System.Type originalType ) Microsoft.CSharp.RuntimeBinder.SymbolTable.GetConstructedType( System.Type type, Microsoft.CSharp.RuntimeBinder.Semantics.AggregateSymbol agg ) 

 Microsoft.CSharp.RuntimeBinder.Semantics.TypeManager.SubstTypeCore( Microsoft.CSharp.RuntimeBinder.Semantics.CType type, Microsoft.CSharp.RuntimeBinder.Semantics.SubstContext pctx ) Microsoft.CSharp.RuntimeBinder.Semantics.TypeManager.SubstTypeArray( Microsoft.CSharp.RuntimeBinder.Semantics.TypeArray taSrc, Microsoft.CSharp.RuntimeBinder.Semantics.SubstContext pctx ) 

如果我不得不冒险猜测,你已经进行的一些通用类型约束嵌套已经设法混淆了绑定器以递归方式处理约束中涉及的类型以及约束本身。

继续并在Connect上提交错误; 如果编译器没有被此捕获,运行时绑定程序可能也不应该。


此代码示例正确运行:

 class Program { static void Main() { dynamic obj = new Second(); Print(obj); } static void Print(object obj) { } } internal class First where T : First { } internal class Second : First> { } 

这让我相信(不了解运行时绑定程序的内部结构)它主动检查递归约束,但只有一个级别。 在中间类之间,绑定器最终没有检测到递归并试图转向它。 (但这只是一个有根据的猜测。我会将它作为附加信息添加到您的Connect错误中,看看它是否有帮助。)

问题是你从自己派生一个类型:

 abstract class SetTreeNode : KeyTreeNode, TKey> { } 

类型SetTreeNote变为KeyTreeNode,TKey>变为KeyTreeNode,TKey>,TKey>并且这一直持续到堆栈溢出为止。

我不知道你要通过使用这个复杂的模型来完成什么,但那是你的问题。

我设法将它减少到这个失败的例子:

 interface ISortedSet { } sealed class SetTree : BalancedTree>, ISortedSet { } abstract class BalancedTree { } abstract class SetTreeNode : KeyTreeNode, TKey> { } abstract class KeyTreeNode : TreeNode { } abstract class TreeNode { } 

然后我通过这样做修复它:

 interface ISortedSet { } sealed class SetTree : BalancedTree>, ISortedSet { } abstract class BalancedTree { } abstract class SetTreeNode : KeyTreeNode { } abstract class KeyTreeNode : TreeNode { } abstract class TreeNode { } 

两者之间的唯一区别是我用KeyTreeNode, TKey>替换了KeyTreeNode, TKey> KeyTreeNode