通过动态访问泛型类型的成员时出现StackOverflowException:.NET / C#框架错误? - c#

在程序中,我使用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>接口,则该程序通常将打印“设置”,并为其他任何内容打印“对象”。但是,使用以下声明,而是抛出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,并且非常无法提前确定是否将引发异常(从而终止进程!),因此引发dynamic非常令人烦恼。

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

调试并切换到“反汇编模式”时,我会看到以下信息:

在该位置注册转储:

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,但我想知道这里发生了什么。我的类声明在某种程度上不受支持吗?

不知道为什么会这样,这使我担心我们在其他地方使用关键字的情况。我可以完全不相信吗?

参考方案

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

是。

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

好的,让我们从一些显而易见的东西开始,以增强编译器出了什么问题。如您所知,使用类似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类型的相同工作中,编译器必须填充三个插槽:

T上定义的Derived<>,用long填充。
T上定义的Base<,>被在T上定义的Derived<>填充,在long上被U填充。
Base<,>上定义的intDerived<long>填充。

当考虑嵌套类,长继承链,从其他泛型类型派生的泛型类型并添加其他泛型参数等等时,您会看到有很多不同的排列需要覆盖。如果您以new List<int>((int)x)开头并且必须回答“类的基本类型是什么?”的问题。 (显然,编译器需要考虑很多),然后必须解决所有这些问题。

动态编译器基于Roslyn之前的静态编译器,该静态编译器基于实际上是用C ++而不是C#编写的静态编译器(仍然有很多动态编译器在C#中散发出C ++的味道) )。可以认为端点(可以执行的代码)比起点更相似。从静态对象和标志表示的类型和操作开始,静态编译器必须解析一堆文本才能理解涉及的类型和操作,而动态编译器则需要解析这些文本。

他们俩都需要知道的一件事是,如果多次提到一个类型,那么它就是同一类型(毕竟,这几乎是类型含义的最基本定义)。如果我们编译int,除非它知道Dictionary<int, int>两次都是相同的意思,否则显然是行不通的。他们还需要避免消耗大量的RAM。

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

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

一个表示类型的符号,没有特定的类型参数(这是非泛型类型的表示的总和),但确实包含泛型定义的类型参数(对于TKey,它的TValueDictionary<TKey, TValue>T)。
类型的集合,它们是直接在类型上的参数(对于开放类型,是List<T>int,对于构造类型是List<int>Dictionary<T, int>,还是例如相对于T的混合一些定义List<int>.Enumerator的通用类型或方法。
类型参数集,直接位于类型(如上)或嵌套在其上的外部类型上。

好,到目前为止,很好。如果需要使用List<T>做某事,它首先在商店中找到List<T>.Enumerator符号,或者如果它是新符号,则添加它,然后在商店中找到int符号,或者如果它是新符号,则添加它,然后找到int在商店中存储List<T>.Enumerator(作为非常常见的类型预加载),最后在商店中找到将intList<int>.Enumerator组合在一起的类型,或者在新的情况下添加它。现在,我们只有唯一的object类型对象。

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

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

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

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

即使在非常复杂的层次结构中,此方法也能很好地工作。但是,如果存在一个在类型参数方面都是圆形的层次结构,并且在到达非泛型类型(例如StackOverflowException)之前有一个以上的步骤(因此,修复也必须基于基本类型进行递归) ),那么它就无法找到它正在制作的类型(记住有关存储对象的类型),因为它已经临时更改以使修复工作正常进行,必须重新制作。一遍又一遍,直到您按下。

从亚当·马拉斯的回答:

这使我相信(对运行时绑定程序的内部知识不多),它可以主动检查递归约束,但仅深入一层。

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

将列表<>写入数据库asp.net - c#

我被困写数据库不要把我丢错写命令不起作用控制器:[HttpPost] public async Task<ActionResult>PartialTabelaEcp(string userDate) { var numerMiesiaca = 1; var numerRoku = 1; var dbExists = _ecpContext.Kar…

Java中的<<或>>>是什么意思? - java

This question already has answers here: Closed 7 years ago. Possible Duplicate: What does >> and >>> mean in Java?我在一些Java代码中遇到了一些陌生的符号,尽管代码可以正确编译和运行,但对于括号在此代码中的作用却感…

ManyToOne关系上的Xamarin.Forms Sqlite-net NotSupportedException“不知道<model>” - c#

我在xamarin.forms应用程序中使用sqlite net扩展库。我在PCL中编写数据库代码和模型。当我调用SQLiteConnection.CreateTable()时出现错误System.NotSupportedException: Don't know about Cigars.Models.CigarSmoke是雪茄的孩子,它与Man…

.NET XPathNavigator找不到XPath查询中指定的元素(但是XPath查询在XMLSpy中有效) - c#

我有以下C#代码段,用于使用XPath在正在输入的XML文件中查找错误:string xml; // the XML is passed as a parameter as the string below using (Stream messageStream = new MemoryStream(xml)) { IXPathNavigable sourc…

菱形运算符<>是否等于<?> - java

我在util.TreeSet类中发现,其中一个构造函数正在使用具有空泛型类型的新TreeMap调用另一个构造函数。 public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); } new TreeMap<>是什么意思…