На первый взгляд, dynamic в C# — просто object с поддержкой машинерии компилятора. Но не совсем.
Ядром времени выполнения является DLR (Dynamic Language Runtime) — подсистема/фреймворк для поддержки динамических языков программирования. Существует реализация под собственно C#, который идет в поставке с .NET, и отдельная для Iron-языков.
Когда мы работаем с обобщениями (generics), то CLR имеет свои оптимизации на предмет специализации оных. В тот момент, когда CLR+DLR должны работать с generics вместе, поведение написанного кода может стать непредсказуемым.
Preamble
Для начала необходимо вспомнить как поддерживаются обобщения CLR’ом. Каждый generic-тип имеет свою реализацию, т.е. отсутствует type-erasure. Но для ссылочных типов среда использует тип System.__Canon для шаринга кода. Это необходимо не столько из-за очевидности (каждый объект — ссылка размером машинное слово), сколько для разрешения циклической зависимости между типами.
Об этом я уже писал:
Дело в том, что generic-типы могут содержать циклические зависимости от других типов, что чревато бесконечным созданием специализаций для кода. Например:
class 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, как таковой, является вещью опасной. В следующий раз мы рассмотрим пример, где его использование — правильно.