The dangers of not looking ahead

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

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

Коли ми працюємо з узагальненнями (узагальнення), то CLR має свої оптимізації на предмет спеціалізації оних. В той момент, коли CLR+DLR повинні працювати з узагальнення разом, поведінка написаного коду може стати непередбачуваним.

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

Про це я вже писав:
Справа в тому, що generic-типи можуть містити циклічні залежності від інших типів, що загрожує нескінченним створенням спеціалізацій для коду. Наприклад:
Узагальнення 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, хоч і зазнав до цього моменту зміни. Приклад використовувався під час мого виступу «Ефективне використання 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, як такої, є річчю небезпечною. Наступного разу ми розглянемо приклад, де його використання — правильно.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.