Ядром времени выполнения является DLR (Dynamic Language Runtime) — подсистема/фреймворк для поддержки динамических языков программирования. Существует реализация под собственно C#, который идет в поставке с .NET, и отдельная для Iron-языков.
Когда мы работаем с обобщениями (generics), то CLR имеет свои оптимизации на предмет специализации оных. В тот момент, когда CLR+DLR должны работать с generics вместе, поведение написанного кода может стать непредсказуемым.
Preamble
Для начала необходимо вспомнить как поддерживаются обобщения CLR'ом.
Каждый generic-тип имеет свою реализацию, т.е. отсутствует type-erasure. Но для ссылочных типов среда использует тип System.__Canon для шаринга кода. Это необходимо не столько из-за очевидности (каждый объект — ссылка размером машинное слово), сколько для разрешения циклической зависимости между типами.
Об этом я уже писал:
Дело в том, что generic-типы могут содержать циклические зависимости от других типов, что чревато бесконечным созданием специализаций для кода. Например:
Generics cyclomatic dependenciesclass GenericClassOne<T> { private T field; } class GenericClassTwo<U> { private GenericClassThree<GenericClassOne<U>> field } class GenericClassThree<S> { private GenericClassTwo<GenericClassOne<S>> field } class Program { static void Main(string[] args) { Console.WriteLine((new GenericClassTwo<object>()).ToString()); Console.Read(); } }
Однако этот код не упадет и выведет GenericClassTwo`1[System.Object].
Type loader (он же загрузчик типов) сканирует каждый generic-тип на наличие циклической зависимости и присваивает очередность (т.н. LoadLevel для класса). Хотя все специализации для ref-types имеют System.__Canon как аргумент типа — это следствие, а не причина.
Фазы загрузки (они же ClassLoadLevel):
enum ClassLoadLevel { CLASS_LOAD_BEGIN, CLASS_LOAD_UNRESTOREDTYPEKEY, CLASS_LOAD_UNRESTORED, CLASS_LOAD_APPROXPARENTS, CLASS_LOAD_EXACTPARENTS, CLASS_DEPENDENCIES_LOADED, CLASS_LOADED, CLASS_LOAD_LEVEL_FINAL = CLASS_LOADED, };
Infinite loop
Раз такая особенность существует для обобщений, соответственно, и др. подсистемы также должны следовать этому правилу. Но DLR — исключение.
Рассмотрим иерархию классов:
NB: код реальный — из проекта structuremap, хоть и претерпевший к этому моменту изменения. Пример использовался во время моего выступления «Эффективное использование DLR».
public class LambdaInstance<T> : LambdaInstance<T, T>
{
}
public class LambdaInstance<T, TPluginType>
: ExpressedInstance<LambdaInstance<T, TPluginType>, T, TPluginType>
{
}
public abstract class ExpressedInstance<T>
{
}
public abstract class ExpressedInstance<T, TReturned, TPluginType> : ExpressedInstance<T>
{
}
И непосредственно код:
class Program
{
static LambdaInstance<object> ShouldThrowException(object argument)
{
throw new NotImplementedException();
}
static void Main(string[] args)
{
// будет ли брошено исключение?
ShouldThrowException((dynamic)new object());
}
}
Вопрос: будет ли брошено исключение?
Ответ: нет. Метод ShouldThrowException никогда не завершится. И stackoverflow (
Хм… Так в чем же дело? — спросите Вы.
Все просто — LambdaInstance<object>. Рассмотрим иерархию классов еще раз.
LambdaInstance<T> наследуется от LambdaInstance<T, TPluginType>, который в свою очередь от ExpressedInstance<LambdaInstance<T, TPluginType>, T, TPluginType>.
Вложенное наследование заметили?
Как уже говорилось выше, CLR имеет оптимизацию для циклических зависимостей типов.
Для выражения
ShouldThrowException((dynamic)new object());
DLR должен проинспектировать участок кода/сигнатуру метода. В этом процессе встречается LambdaInstance<object> и код превращается в бесконечный цикл.Почему не крешится? DLR не использует рекурсию. Более того, потребление памяти растет (ибо создаются доп. метаданные), но не сильно.
Epilog
Может показаться, что dynamic, как таковой, является вещью опасной. В следующий раз мы рассмотрим пример, где его использование — правильно.
Комментарии (13)
Saladin
09.04.2016 16:40Спасибо за статью!
Но у меня осталась пара вопросов:
- DLR и CLR используют один и тот же TypeLoader?
- Почему DLR не использует рекурсию? Это какое-то фундаментальное ограничение, или команда DLR планирует реализовать эту функциональность в будущем?
szKarlen
09.04.2016 17:22+1- TypeLoader один и тот же
- На самом деле это не ограничение. Нерекурсивный (почти) алгоритм, используемый в DLR дает одновременно и плюс, и минус в виде отсутствии переполнения стека. К сожалению, разработка непосредственно DLR идет неактивно и только в ветке IronPython. По данному вопросу новостей нет
Saladin
09.04.2016 18:23А не подскажите где можно найти исходный код собственно самого TypeLoader'a?
Я полазил по проекту coreclr и исходникам .NET'a, но нашел, только вот это:
Но оно все как-то мало подходит под то, о чем вы говорите.
szKarlen
09.04.2016 19:05+1Код загрузки типов в основном сосредоточен в секции vm. Несколько размазан, но конкретно по теме: загрузка generics. Вместо метода CheckInstantiationForRecursion из проекта Rotor (sscli20) код стал более ООП-шным и теперь используется граф (RecursionGraph).
кстати, по архитектуре загрузчика типов более подробно.
netaholic
09.04.2016 19:06Здравствуйте. Спасибо за интересную статью.
А не могли бы вы поподробнее объяснить вот эти слова:
Type loader (он же загрузчик типов) сканирует каждый generic-тип на наличие циклической зависимости и присваивает очередность (т.н. LoadLevel для класса). Хотя все специализации для ref-types имеют System.__Canon как аргумент типа — это следствие, а не причина.
Когда я писал кодогенерацию на Mono.Cecil с использованием generic'ов — ни с чем таким не сталкивался.szKarlen
09.04.2016 19:15+2Тип
System.__Canon
никогда не виден ни при написании MSIL'a, ни при приведении типов. Данная особенность является внутренней оптимизацией CLR и может не применяться др. реализациями. так это позволяет упростить сам процесс выявления зависимостей между типами загрузчиком, т.к. происходит трекинг лишьSystem.__Canon
.
p.s.
Это никак не влияет на кодогенерацию MSIL будь то Mono.Cecil, либо Reflection.Emit и т.п.
Mixim333
10.04.2016 17:26Спасибо, познавательно.
«В следующий раз мы рассмотрим пример, где его использование — правильно.» — вот это больше всего интересно. Сам всегда стараюсь избегать типа dynamic — боюсь сам себе «в ногу выстрелить», хотя тип очень интересный. К тому же, читал, по-моему, у Рихтера, что работа с dynamic добавляет кучу накладных расходов и что его не рекомендуется использовать
Shablonarium
Это перевод?
szKarlen
нет, а почему так решили?
dkukushkin
Видимо потому что большая часть статей с глубоким капанием — заморские.
xaizek
Я думаю всё проще: название и заголовки на английском написаны.