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

时间:2014-03-26 20:51:13

标签: c# .net dynamic

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

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

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

    static void Print<TKey>(ISortedSet<TKey> obj)
    {
        Console.WriteLine("set");
    }
}

如果新建的实例实现ISortedSet<TKey>接口并打印“对象”,则该程序通常打印“set”。但是,通过以下声明,将引发StackOverflowException(如上面的注释中所述)。

interface ISortedSet<TKey> { }

sealed class SetTree<TKey> : BalancedTree<SetTreeNode<TKey>>, ISortedSet<TKey> {}

abstract class BalancedTree<TNode> 
    where TNode : TreeNode<TNode> { }

abstract class SetTreeNode<TKey> : KeyTreeNode<SetTreeNode<TKey>, TKey> { }

abstract class KeyTreeNode<TNode, TKey> : TreeNode<TNode>
    where TNode : KeyTreeNode<TNode, TKey> { }

abstract class TreeNode<TNode>
    where TNode : TreeNode<TNode> { }

这是否是一个bug是非常令人不安的,因为我们无法捕获StackOverflowException,并且几乎无法事先确定是否会抛出异常(从而终止过程!)。

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

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

disassembly

在该位置注册转储: register dump

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

这并没有告诉我更多的指示,这确实必须是框架中的某种错误。

filed a bug report on Microsoft Connect但我很想知道这里发生了什么。我的类声明是否在某种程度上不受支持?

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

3 个答案:

答案 0 :(得分:7)

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

class Program
{
    static void Main()
    {
        dynamic obj = new Third<int>();
        Print(obj); // causes stack overflow
    }

    static void Print(object obj) { }
}

class First<T> where T : First<T> { }

class Second<T> : First<T> where T : First<T> { }

class Third<T> : Second<Third<T>> { }

查看调用堆栈,它似乎在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<int>();
        Print(obj);
    }

    static void Print(object obj) { }
}

internal class First<T>
    where T : First<T> { }

internal class Second<T> : First<Second<T>> { }

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

答案 1 :(得分:3)

问题在于您从自身派生类型:

abstract class SetTreeNode<TKey> : KeyTreeNode<SetTreeNode<TKey>, TKey> { }

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

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

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

interface ISortedSet<TKey> { }

sealed class SetTree<TKey> : BalancedTree<SetTreeNode<TKey>>, ISortedSet<TKey> { }

abstract class BalancedTree<TNode> { }

abstract class SetTreeNode<TKey> : KeyTreeNode<SetTreeNode<TKey>, TKey> { }

abstract class KeyTreeNode<TNode, TKey> : TreeNode<TNode> { }

abstract class TreeNode<TNode> { }

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

interface ISortedSet<TKey> { }

sealed class SetTree<TKey> : BalancedTree<SetTreeNode<TKey>>, ISortedSet<TKey> { }

abstract class BalancedTree<TNode> { }

abstract class SetTreeNode<TKey> : KeyTreeNode<TKey, TKey> { }

abstract class KeyTreeNode<TNode, TKey> : TreeNode<TNode> { }

abstract class TreeNode<TNode> { }

两者之间的唯一区别是我将KeyTreeNode<SetTreeNode<TKey>, TKey>替换为KeyTreeNode<TKey, TKey>

答案 2 :(得分:1)

  

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

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

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

现在,考虑一下:

public class Base<T, U>
{

}

public class Derived<T> : Base<T, int>
{

}

Derived<long> l = new Derived<long>();

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

  1. T上定义的Derived<>,其中填充了long
  2. T上定义的Base<,>充满了T上定义的Derived<>,其中填充了long
  3. U上定义的Base<,>充满了int
  4. 当您考虑嵌套类,长继承链,从其他泛型类型派生的泛型类型以及添加更多通用参数等时,您可以看到有很多不同的排列可供覆盖。如果您从Derived<long>开始并且必须回答问题&#34;该类的基本类型是什么?&#34; (显然编译器需要考虑很多)然后所有这些都必须解决。

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

    他们都需要知道的一件事是,如果一个类型被多次提及,它就是相同的类型(毕竟这是类型意味着什么的最基本的定义,毕竟)。如果我们编译显然不起作用的new List<int>((int)x),除非它知道int两次意味着相同的事情。他们还需要避免咀嚼内存。

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

    对于大多数类型(除了一些特殊情况,如指针,引用,数组,nullables [虽然那个例外是例外],输入参数......好吧,实际上有很多例外)国家大多是三件事:

    1. 表示没有特定类型参数的类型的符号(这是非泛型类型的表示的总和),但它包含泛型定义的类型参数(对于Dictionary<int, int>,它具有{{1 {}}和TKey的{​​{1}}。
    2. 直接在该类型上作为参数的类型集(对于开放类型,TValueDictionary<TKey, TValue>,对于构造类型,TList<T> ,或相对于定义int的某些泛型类型或方法的List<int>的混合。
    3. 直接在类型上的类型参数集(如上所述)或嵌套在其上的外部类型。
    4. 好的,到目前为止,这么好。如果需要对Dictionary<T, int>执行某项操作,则首先在商店中找到T符号,或者在新商品中添加该符号,然后在商店中找到List<int>.Enumerator符号,或者在新商品中添加,然后在商店中找到List<T>List<T>.Enumerator作为一种非常常见的类型预加载),最后在商店中找到将intint结合的类型,或者添加它如果新的。我们现在有唯一的 List<T>.Enumerator类型对象。

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

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

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

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

      即使是非常复杂的层次结构,这也很有效。但是,如果在类型参数方面具有循环的层次结构在达到非泛型类型(例如int)之前有多个步骤(那么修复也必须在基础类型上递归)然后它无法在制作过程中找到它的类型(记住关于存储类型对象的位),因为它&#39 ; s已被暂时更改以进行修复工作,并且必须再次进行修复。再一次,直到你点击List<int>.Enumerator

      来自Adam Maras&#39;回答:

        

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

      它几乎恰恰相反,因为问题是主动设置基类,以防止它意识到它已经拥有了它需要的类型。 I think I managed to fix it today虽然还有待观察是否有人看到我错过了该修复的一些问题(对框架做出贡献的好处是他们有高标准的代码审查,但这当然意味着我可以&#39;确保捐款在被接受之前被接受。