Ответ 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
ofDictionary<TKey, TValue>
). - Набор типов, которые являются параметрами непосредственно для типа (будь то
T
дляList<T>
для открытого типа,int
ofList<int>
для построенного типа или сочетание, например,Dictionary<T, int>
относительно некоторого общего типа или метода, который определяетT
). - Набор параметров типа, которые либо непосредственно относятся к типу (как указано выше), либо к внешнему типу, в который он вложен.
Хорошо, до сих пор, так хорошо. Если ему нужно что-то сделать с помощью List<int>.Enumerator
, он сначала либо находит символ List<T>
в хранилище, либо добавляет его, если новый, а затем находит List<T>.Enumerator
символ в хранилище или добавляет его, если новый, а затем находит int
в хранилище (int
предварительно загружается как очень распространенный тип) и, наконец, находит тип, который объединяет List<T>.Enumerator
с int
в хранилище или добавляет его, если новый. Теперь у нас есть единственный объект List<int>.Enumerator
.
Проблема, вызвавшая вашу ошибку, появляется в конце этого последнего шага. Подумайте, что мы сказали выше о необходимости присваивать типы базовым типам при создании конкретной реализации типа. Базовый тип конкретного родового типа - это конкретный тип, потенциально сам по себе конкретный общий тип, но информация, которую мы здесь имеем, относится к родовому типу и некоторым аргументам типа: мы не знаем, что такое конкретный общий тип.
Метод поиска базового типа ленивый, но вызывает символ, который не знает параметры типа, которые нужно использовать.
Используемое решение заключалось в том, чтобы временно определить этот базовый тип символа в терминах конкретного базового типа, вызвать метод ленивого загрузки базового типа и затем снова установить его.
Я не знаю, почему что-то было лениво загружено, когда оно было вызвано сразу после создания. Полагаю, что я бы сказал, что это было более разумным с точки зрения статической компиляции, и поэтому портировано таким образом, а не переписывать механизм с нуля (что было бы более рискованным подходом в большинстве случаев).
Это работает очень хорошо, даже с довольно сложными иерархиями. Однако если иерархия, которая является круговой в терминах параметров типа и, имеет более одного шага до того, как будет достигнут не общий тип (например, object
) (поэтому исправление имеет для повторной обработки на базовом типе), то он не может найти тип, который он производит (помните бит о хранении объектов для типов), потому что он был временно изменен для выполнения исправления и имеет чтобы сделать это снова. И снова, и снова, пока вы не нажмете StackOverflowException
.
От ответа Адама Мараса:
Это заставляет меня поверить (без особого знания внутренних компонентов связующего времени выполнения), что он проактивно проверяет рекурсивные ограничения, но только на один уровень.
Это почти наоборот, поскольку проблема заключается в том, что проблема заключается в том, что базовый класс задает базовый класс таким образом, чтобы он не осознавал, что у него уже есть тип, который ему нужен. Мне кажется, мне удалось исправить это сегодня, хотя это еще предстоит выяснить, если кто-то видит какую-то проблему с этим исправлением, которое я пропустил (хорошая вещь о том, чтобы внести свой вклад к рамочному принципу они имеют высокий стандарт обзоров кода, но это, конечно, означает, что я не могу быть уверен, что вклад будет принят до тех пор, пока он не зайдет).