【问题标题】:StackOverflowException when accessing member of generic type via dynamic: .NET/C# framework bug?通过动态访问泛型类型成员时出现 StackOverflowException:.NET/C# 框架错误?
【发布时间】:2014-03-26 20:51:13
【问题描述】:

在一个程序中,我使用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&lt;TKey&gt; 接口,则该程序通常会打印“set”并打印“object”以获取其他任何内容。但是,使用以下声明会抛出 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> { }

无论这是否是一个错误,抛出StackOverflowException 是非常令人不安的,因为我们无法捕获它并且几乎无法提前确定是否会抛出异常(从而终止进程! )。

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

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

在该位置注册转储:

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 关键字。我可以不相信吗?

【问题讨论】:

  • C# 是否支持基于动态调用重载,我不知道,但您可以通过 RTTI 手动完成。在 (object obj) 重载中,检查类型、强制转换并将其传递给适当的重载。抱歉,如果这没有帮助。您可以将强制转换为动态和对 Print 的调用分离为单独的语句吗?这将有助于确定是导致问题的动态转换,还是试图找到合适的打印重载。
  • 像这样的动态方法调用工作正常通常。只是它显然有时会导致堆栈溢出。
  • @Brent:是的,我会更新代码。同样的行为。
  • 假设您使用的是 Visual Studio,请关闭“Step into just my code”并尝试进入通话。如果这不能让您观察到堆栈溢出的发生,请尝试切换到反汇编。即使您不了解汇编程序,您也应该能够在调用堆栈崩溃之前观察到大量递归链。如果它是在非你的代码中完成这一切,那么我建议创建一个错误报告。

标签: c# .net dynamic


【解决方案1】:

我创建了一个更短、更中肯的 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 错误中,看看是否有帮助。)

【讨论】:

  • @MårtenWikström 我刚刚添加了一个工作示例,并冒险猜测发生了什么。也许你也可以包括在内。
  • 可能应该等待 Jon Skeet 醒来并在提交错误报告之前回答您的问题... :)
  • “这让我相信(对运行时绑定器的内部没有太多了解)它正在主动检查递归约束,但只有一层深度。”几乎相反。它主动设置基本类型的详细信息,但这将在层次结构中的两个以上泛型类型上递归,并且这种递归情况无法处理循环定义。
  • 顺便说一句,这两对方法有点红鲱鱼。当正常工作时,这两个都可以递归几个级别,使用相对大量的堆栈。这使得他们最有可能在TypeManager.GetAggregate() 中的问题比应有的递归次数更多时处于堆栈的末尾,因此尽管他们是无辜的,但他们看起来像是罪魁祸首。
【解决方案2】:

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

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

SetTreeNote&lt;TKey&gt; 类型变为 KeyTreeNode&lt;SetTreeNode&lt;TKey&gt;,TKey&gt;,再变为 KeyTreeNode&lt;KeyTreeNode&lt;SetTreeNode&lt;TKey&gt;,TKey&gt;,TKey&gt;,这种情况一直持续到堆栈溢出。

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

我设法将其简化为这个失败的例子:

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&lt;SetTreeNode&lt;TKey&gt;, TKey&gt;换成了KeyTreeNode&lt;TKey, TKey&gt;

【讨论】:

  • abstract class SetTreeNode&lt;TKey&gt; : KeyTreeNode&lt;SetTreeNode&lt;TKey&gt;, TKey&gt; { }: +1,我就是这么想的。
  • 那么也许存在编译器实际编译它而不是禁止它的错误?
  • 如果是这样,那为什么代码不仅可以编译,还允许实例化类型?
  • @Dan-o 但是...不是运行时崩溃,而是 C# binder。堆栈跟踪证明了这一点。如果运行时本身在实例化该类型的对象时遇到问题,1) 堆栈跟踪将显示来自 mscorlib 的方法或 CLR 内的本机代码,以及 2) 它很可能以 TypeLoadException 结束。
  • 谢谢。我撤回了我的评论。
【解决方案3】:

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

是的。

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

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

现在,考虑:

public class Base<T, U>
{

}

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

}

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

在这里您可以看到,在 Derived&lt;T&gt;long 类型的相同工作中,编译器必须填充三个插槽:

  1. Derived&lt;&gt; 上定义的T,由long 填充。
  2. Base&lt;,&gt; 上定义的TDerived&lt;&gt; 上定义的T 填充,long 填充。
  3. Base&lt;,&gt; 上定义的Uint 填充。

当您考虑嵌套类、长继承链、从其他泛型类型派生的泛型类型以及添加更多泛型参数等等时,您会发现有很多不同的排列需要涵盖。如果您从Derived&lt;long&gt; 开始并且必须回答“类的基本类型是什么?”这个问题。 (显然编译器需要考虑很多)然后所有这些都必须解决。

动态编译器基于 Roslyn 之前的静态编译器,该编译器基于之前的编译器,实际上是用 C++ 而不是 C# 编写的(仍然有相当多的动态编译器是用 C# 编写的, C++ 的气味)。可以认为终点(可以执行的代码)比起点更相似;静态编译器必须解析一堆文本,以了解所涉及的类型和操作,而动态编译器则从对象和标志表示的现有类型和操作开始。

他们都需要知道的一件事是,如果一个类型被多次提及,那么它就是同一个类型(毕竟,这几乎是对类型含义的最基本定义)。如果我们编译new List&lt;int&gt;((int)x),那显然是行不通的,除非它知道int 两次都意味着同样的事情。他们还需要避免占用大量 RAM。

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

对于大多数类型(除了一些特殊情况,如指针、引用、数组、可空值[尽管该异常有一个例外]、类型参数……好吧,实际上有很多例外),状态主要是三件事:

  1. 表示没有特定类型参数的类型的符号(这是非泛型类型的全部表示)但确实包括泛型定义的类型参数(对于Dictionary&lt;int, int&gt;,它具有TKey 和@ 987654345@ Dictionary&lt;TKey, TValue&gt;)。
  2. 直接作为类型参数的类型集(无论是开放类型的List&lt;T&gt;T,构造类型的List&lt;int&gt;int,还是例如@987654351 的混合@相对于定义T的一些泛型类型或方法。
  3. 直接位于类型(如上)或嵌套在其内的外部类型上的类型参数集。

好的,到目前为止,一切都很好。如果它需要对List&lt;int&gt;.Enumerator 做某事,它首先在商店中找到List&lt;T&gt; 符号,或者添加它(如果是新符号),然后在商店中找到List&lt;T&gt;.Enumerator 符号,或者添加它(如果是新符号),然后找到int在 store 中(int 被预加载为一个非常常见的类型),最后在 store 中找到 List&lt;T&gt;.Enumeratorint 组合的类型,或者如果是新的则添加它。我们现在有了唯一的 List&lt;int&gt;.Enumerator 类型对象。

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

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

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

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

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

来自亚当马拉斯的回答:

这让我相信(对运行时绑定器的内部没有太多了解)它正在主动检查递归约束,但只有一层深度。

几乎相反,问题在于主动设置基类以防止它意识到它已经具有所需的类型。 I think I managed to fix it today 尽管有人发现我错过的修复有什么问题还有待观察(为框架做出贡献的好处是他们有高标准的代码审查,但这当然意味着我不能确定贡献将被接受,直到它进入)。

【讨论】:

  • 感谢您提供这个写得很好的和有教育意义的答案!
  • 感谢您的提问。在弄清楚这一点之后(尽管困扰了我很长时间,但它突然变得足够流畅了),然后按照 github 中相关项目的链接查看是否有任何变体案例我应该在测试中考虑,很高兴有一个问题问什么我只是花时间弄清楚。现在等待看看我的修复是否会引入一些我没有注意到的更严重的缺陷。 (如果这是一个很好的修复,是否将它从 corefx 移植到 netfx 以解决您的 Microsoft Connect 问题是另一回事;netfx 对更改的谨慎程度高于 corefx)。
  • 万岁,修复被接受。 .NET Core 的下一个版本将修复此问题,但我不能说它是否会成为桌面版本的另一个版本。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-15
  • 1970-01-01
  • 2011-05-07
  • 1970-01-01
  • 2010-09-06
  • 1970-01-01
  • 2018-07-29
相关资源
最近更新 更多