- PVSM.RU - https://www.pvsm.ru -

The dangers of not looking ahead

На первый взгляд, dynamic в C# — просто object с поддержкой машинерии компилятора. Но не совсем.

Ядром времени выполнения является DLR (Dynamic Language Runtime) — подсистема/фреймворк для поддержки динамических языков программирования. Существует реализация под собственно C#, который идет в поставке с .NET и отдельная для Iron-языков.

Когда мы работаем с обобщениями (generics), то CLR имеет свои оптимизации на предмет специализации оных. В тот момент, когда CLR+DLR должны работать с generics вместе, поведение написанного кода может стать непредсказуемым.

Preamble

Для начала необходимо вспомнить как поддерживаются обобщения CLR'ом.
Каждый generic-тип имеет свою реализацию, т.е. отсутствует type-erasure. Но для ссылочных типов среда использует тип System.__Canon для шаринга кода. Это необходимо не столько из-за очевидности (каждый объект — ссылка размером машинное слово), сколько для разрешения циклической зависимости между типами.

Об этом я уже писал [1]:

Дело в том, что generic-типы могут содержать циклические зависимости от других типов, что чревато бесконечным созданием специализаций для кода. Например:

Generics cyclomatic dependencies

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 [2], хоть и претерпевший к этому моменту изменения. Пример использовался во время моего выступления [3] «Эффективное использование 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&ltobject&gt. Рассмотрим иерархию классов еще раз.

LambdaInstance&ltT&gt наследуется от LambdaInstance&ltT, TPluginType&gt, который в свою очередь от ExpressedInstance&ltLambdaInstance&ltT, TPluginType&gt, T, TPluginType&gt.

Вложенное наследование заметили?

Как уже говорилось выше, CLR имеет оптимизацию для циклических зависимостей типов.

Для выражения ShouldThrowException((dynamic)new object()); DLR должен проинспектировать участок кода/сигнатуру метода. В этом процессе встречается LambdaInstance&ltobject&gt и код превращается в бесконечный цикл.

Почему не крешится? DLR не использует рекурсию. Более того, потребление памяти растет (ибо создаются доп. метаданные), но не сильно.

Epilog

Может показаться [4], что dynamic как таковой является вещью опасной. В следующий раз мы рассмотрим пример, где его использование — правильно.

Автор: szKarlen

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/117490

Ссылки в тексте:

[1] писал: https://habrahabr.ru/post/253105/

[2] structuremap: http://structuremap.github.io/

[3] выступления: http://msk2014.dotnext.ru/

[4] показаться: https://habrahabr.ru/post/280234/

[5] Источник: https://habrahabr.ru/post/281274/