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

Mår*_*röm 11 .net c# 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果新建的实例实现了接口并打印了"对象",则该程序通常会打印"set" ISortedSet<TKey>.但是,通过以下声明,将StackOverflowException抛出a (如上面的注释中所述).

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> { }
Run Code Online (Sandbox Code Playgroud)

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

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

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

拆卸

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

EAX = 02B811B4 EBX = 0641EA5C ECX = 02C3B0EC EDX = 02C3A504 ESI = 02C2564C
EDI = 0641E9AC EIP = 011027B9 ESP = 0641E91C EBP = 0641E9B8 EFL = 00000202
Run Code Online (Sandbox Code Playgroud)

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

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

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

Ada*_*ras 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>> { }
Run Code Online (Sandbox Code Playgroud)

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

Microsoft.CSharp.RuntimeBinder.SymbolTable.LoadSymbolsFromType(
    System.Type originalType
)

Microsoft.CSharp.RuntimeBinder.SymbolTable.GetConstructedType(
    System.Type type,
    Microsoft.CSharp.RuntimeBinder.Semantics.AggregateSymbol agg
)
Run Code Online (Sandbox Code Playgroud)

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
)
Run Code Online (Sandbox Code Playgroud)

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

继续并在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>> { }
Run Code Online (Sandbox Code Playgroud)

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

  • 提交的错误:https://connect.microsoft.com/VisualStudio/feedback/details/841413 (3认同)

Jon*_*nna 1

\n

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

\n
\n\n

是的。

\n\n

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

\n\n

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

\n\n

现在,考虑:

\n\n
public class Base<T, U>\n{\n\n}\n\npublic class Derived<T> : Base<T, int>\n{\n\n}\n\nDerived<long> l = new Derived<long>();\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
    \n
  1. T上定义的,Derived<>会被 填充long
  2. \n
  3. 定义T的 onBase<,>被填充T定义的 onDerived<>被填充long
  4. \n
  5. U上的定义Base<,>被填充int
  6. \n
\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n\n
    \n
  1. 表示没有特定类型参数的类型的符号(这是非泛型类型表示的总体),但包含泛型定义的类型参数(因为Dictionary<int, int>它具有TKeyTValueDictionary<TKey, TValue>
  2. \n
  3. 直接作为类型参数的类型集(无论是开放类型的Tof 、构造类型的of ,还是例如相对于定义的某些泛型类型或方法的混合)List<T>intList<int>Dictionary<T, int>T混合)。
  4. \n
  5. 直接位于类型上(如上所述)或位于其嵌套的外部类型上的类型参数集。
  6. \n
\n\n

好吧,到目前为止,一切都很好。List<int>.Enumerator如果需要首先对其执行某些操作,则可以List<T>在商店中查找符号,或者如果是新的则添加它,然后List<T>.Enumerator在商店中查找符号,或者如果是新的则添加它,然后int在商店中查找(int作为非常常见的类型预加载)最后找到List<T>.Enumeratorint商店中组合的类型,如果是新的则添加它。我们现在拥有唯一 List<int>.Enumerator类型对象。

\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n\n

即使层次结构非常复杂,这种方法也能很好地工作。但是,如果存在一个层次结构,该层次结构在类型参数方面既是循环的,并且在到达非泛型类型(例如 )之前有多个步骤object(因此修复也必须在基本类型上递归)那么它就无法找到正在制作的类型(记住有关存储类型对象的部分),因为它已被临时更改以进行修复工作,并且必须再做一次。一次又一次,直到你击中StackOverflowException

\n\n

Adam Maras 的回答:

\n\n
\n

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

\n
\n\n

这几乎是相反的,因为问题是主动设置基类,以防止它意识到它已经拥有它需要的类型。我想我今天成功解决了这个问题尽管是否有人发现我错过的修复存在一些问题还有待观察(为框架做出贡献的一个好处是他们有高标准的代码审查,但这当然意味着我可以在提交之前不能确定捐款是否会被接受)。

\n