StackOverflowException при доступе к члену родового типа через динамический: ошибка .NET/С# framework?

В программе я использую ключевое слово 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> и печатает "объект" для чего-либо еще. Но, вместо этого, вместо этого вызывается a 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 выбрасывается, поскольку мы не можем его поймать, а также в значительной степени неспособным заранее определить, будет ли исключено исключение (и тем самым прекратит процесс!).

Может кто-нибудь объяснить, что происходит? Является ли это ошибкой в ​​рамках?

При отладке и переключении в "Режим разборки" я вижу следующее:

disassembly

Зарегистрировать дамп в этом месте: register dump

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

Это не говорит мне больше, чем показатель того, что это действительно должно быть какой-то ошибкой в ​​рамках.

Я написал отчет об ошибке в Microsoft Connect, но мне интересно узнать, что здесь происходит. Являются ли объявления классов некорректными?

Не зная, почему это происходит, я беспокоюсь о других местах, где мы используем ключевое слово dynamic. Могу ли я не доверять этому?

Ответы

Ответ 1

Может кто-нибудь объяснить, что происходит? Является ли это ошибкой в ​​рамках?

Да.

Проблема заключается в том, что общие типы разрешаются в конкретных конкретных целях.

Хорошо, начнем с некоторых очевидных вещей, чтобы создать то, что компилятор ошибается. Как вы знаете, с чем-то вроде List<int> компиляторы (будь то динамический компилятор или какой-либо из статических компиляторов с тех пор, как С# 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<,>, который заполняется с помощью int.

Когда вы рассматриваете вложенные классы, длинные цепочки наследования, общие типы, полученные из других общих типов и добавляющие дополнительные общие параметры, и т.д., вы можете видеть, что есть несколько разных перестановок для покрытия. Если вы начинаете с Derived<long> и должны ответить на вопрос "какой базовый тип класса?" (которые, очевидно, компиляторы должны учитывать много), тогда все это должно быть разработано.

Динамический компилятор был основан на статическом компиляторе pre-Roslyn, который был основан на компиляторе до того, который был фактически написан на С++, а не на С# (до сих пор довольно много динамического компилятора, что, хотя он и в С# запахи С++). Можно считать более похожим в конечной точке (код, который может быть выполнен), чем в начальной точке; кучу текста, который должен быть проанализирован для статического компилятора, чтобы понять, какие типы и операции задействованы против динамического компилятора, начиная с уже существующих типов и операций, представленных объектами и флагами.

Одна вещь, которую они оба должны знать, это то, что если тип упоминается несколько раз, то он тот же тип (что в большинстве своем является самым основным определением того, что означает тип). Если мы скомпилируем new List<int>((int)x), который, очевидно, не будет работать, если бы он не знал, что int означает одно и то же. Им также необходимо избегать жевания гигабайтов оперативной памяти.

Обе проблемы решаются методом хеширования или мухи. Когда он собирается построить объект, представляющий конкретный тип, он сначала видит, если он уже построил этот тип, и при необходимости создает только новый. Это также помогает правильно построить много отношений внутри иерархии, хотя явно не конкретный случай в вашем вопросе.

Для большинства типов (все, за исключением нескольких специальных случаев, таких как указатели, ссылки, массивы, nullables [хотя есть исключение из этого исключения], введите параметры... хорошо, на самом деле существует довольно много исключений), состояние в основном состоит из трех вещей

  • Символ, представляющий тип без конкретных параметров типа (который является совокупностью представления для не общих типов), но который включает в себя параметры типа общего определения (для Dictionary<int, int> он имеет TKey и TValue of Dictionary<TKey, TValue>).
  • Набор типов, которые являются параметрами непосредственно для типа (будь то T для List<T> для открытого типа, int of List<int> для построенного типа или сочетание, например, Dictionary<T, int> относительно некоторого общего типа или метода, который определяет T).
  • Набор параметров типа, которые либо непосредственно относятся к типу (как указано выше), либо к внешнему типу, в который он вложен.

Хорошо, до сих пор, так хорошо. Если ему нужно что-то сделать с помощью List<int>.Enumerator, он сначала либо находит символ List<T> в хранилище, либо добавляет его, если новый, а затем находит List<T>.Enumerator символ в хранилище или добавляет его, если новый, а затем находит int в хранилище (int предварительно загружается как очень распространенный тип) и, наконец, находит тип, который объединяет List<T>.Enumerator с int в хранилище или добавляет его, если новый. Теперь у нас есть единственный объект List<int>.Enumerator.

Проблема, вызвавшая вашу ошибку, появляется в конце этого последнего шага. Подумайте, что мы сказали выше о необходимости присваивать типы базовым типам при создании конкретной реализации типа. Базовый тип конкретного родового типа - это конкретный тип, потенциально сам по себе конкретный общий тип, но информация, которую мы здесь имеем, относится к родовому типу и некоторым аргументам типа: мы не знаем, что такое конкретный общий тип.

Метод поиска базового типа ленивый, но вызывает символ, который не знает параметры типа, которые нужно использовать.

Используемое решение заключалось в том, чтобы временно определить этот базовый тип символа в терминах конкретного базового типа, вызвать метод ленивого загрузки базового типа и затем снова установить его.

Я не знаю, почему что-то было лениво загружено, когда оно было вызвано сразу после создания. Полагаю, что я бы сказал, что это было более разумным с точки зрения статической компиляции, и поэтому портировано таким образом, а не переписывать механизм с нуля (что было бы более рискованным подходом в большинстве случаев).

Это работает очень хорошо, даже с довольно сложными иерархиями. Однако если иерархия, которая является круговой в терминах параметров типа и, имеет более одного шага до того, как будет достигнут не общий тип (например, object) (поэтому исправление имеет для повторной обработки на базовом типе), то он не может найти тип, который он производит (помните бит о хранении объектов для типов), потому что он был временно изменен для выполнения исправления и имеет чтобы сделать это снова. И снова, и снова, пока вы не нажмете StackOverflowException.

От ответа Адама Мараса:

Это заставляет меня поверить (без особого знания внутренних компонентов связующего времени выполнения), что он проактивно проверяет рекурсивные ограничения, но только на один уровень.

Это почти наоборот, поскольку проблема заключается в том, что проблема заключается в том, что базовый класс задает базовый класс таким образом, чтобы он не осознавал, что у него уже есть тип, который ему нужен. Мне кажется, мне удалось исправить это сегодня, хотя это еще предстоит выяснить, если кто-то видит какую-то проблему с этим исправлением, которое я пропустил (хорошая вещь о том, чтобы внести свой вклад к рамочному принципу они имеют высокий стандарт обзоров кода, но это, конечно, означает, что я не могу быть уверен, что вклад будет принят до тех пор, пока он не зайдет).

Ответ 2

Я создал более короткую, более точную 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>> { }

Глядя на стек вызовов, он, кажется, подпрыгивает между двумя парами символов в связующем средстве С#:

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 как дополнительную информацию и посмотрю, поможет ли она.)

Ответ 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>