Ядром времени выполнения является 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, как таковой, является вещью опасной. В следующий раз мы рассмотрим пример, где его использование — правильно.

