Pull to refresh

Заметка про коллекции в C# и структуры. Вопрос памяти

Reading time2 min
Views20K
Данная информация будет полезна тем, кто только разбирается с C#, на практике пытаясь понять теорию, не читая предварительно книжек до конца. Это скорее всего заметка на полях, а не подробное описание и анализ темы. Итак, кому интересно как ведут себя структуры в памяти при работе с коллекциями в C#, добро пожаловать.

Почитывая Рихтера по вечерам, решил более подробно разбираться в каждой детали, имея большое желание понять идеологию .Net глубже. Пролистывая страницы книги, пришел к выводу, что многое, что проектируется и пишется каждый день — постоянно нарушает некоторые основы .Net.

Рассмотрим на примере структур и коллекций. Не буду напоминать, что структуры это value-типы и ведут себя по-другому, нежели ссылочные.

Пример 1. Использование старого ArrayList.

System.Int32 myInt = 7;
 ArrayList al = new ArrayList();
 al.Add(myInt);

Ожидаемым результатом будет наличие двух разных элементов в стеке типа System.Int32. Причем, один из них уже будет ссылочного типа. Если открыть IL-представление данного кода, то увидим справедливое подтверждение:

IL_0002: stloc.0 // myInt
 IL_0003: newobj System.Collections.ArrayList..ctor
 IL_0008: stloc.1 // al
 IL_0009: ldloc.1 // al
 IL_000A: ldloc.0 // myInt
 IL_000B: box System.Int32
 IL_0010: callvirt System.Collections.ArrayList.Add

Операция box(boxing) создает объект в куче типа Int32. При добавлении элемента типа структур, каждый раз будет создаваться его аналог в куче, а ссылка попадать в коллекцию. На помощь этому действию пришли со временем generic-коллекции. Рассмотрим пример и его IL-представление:

Пример 2. Использование обобщений для коллекций:

System.Int32 myInt = 7;
 List<int> l = new List<int>();
l.Add(myInt);

Данный код преобразуется в:

IL_0002: stloc.0 // myInt
 IL_0003: newobj System.Collections.Generic.List..ctor
 IL_0008: stloc.1 // l
 IL_0009: ldloc.1 // l
 IL_000A: ldloc.0 // myInt
IL_000B: callvirt System.Collections.Generic.List.Add

Такого рода коллекции более совершенны и умеют работать с value-типами грамотнее, не используя операции box\unbox. Это значительно сокращает затраты памяти, увеличивает производительность и уменьшает количество сборок мусора.

Однако, поэкспериментировав с коллекциями некоторое время более детально, наткнулся на неожиданный момент. Иногда появляется потребность написать нечто в таком стиле как:

IList l = new List<T>();

Код простой, но результат его для структур оказался неоднозначным.

Если мы посмотрим на IL-представление такого кода:

System.Int32 myInt = 7;
 IList l = new List<int>();
 l.Add(myInt);

То получим следующее:

 IL_0002: stloc.0 // myInt
 IL_0003: newobj System.Collections.Generic.List..ctor
 IL_0008: stloc.1 // l
 IL_0009: ldloc.1 // l
 IL_000A: ldloc.0 // myInt
 IL_000B: box System.Int32
 IL_0010: callvirt System.Collections.IList.Add

Мы опять попадаем на блок упаковки. И снова получим объект в куче как копию нашего значения myInt. Видимо разработчики реализовали коллекции так, что действительно, явное указание типа обобщения для переменной влияет на дальнейшее поведение объекта коллекции, особенно для значимых типов.

Это был первый вывод, которым попытался объяснить происходящее. В итоге объяснение нашлось чуть позже от самого автора. Любая интерфейсная переменная всегда должна быть размещена в куче, что и заставило появиться дополнительной операции упаковки.

Вывод прост. Вначале теория, а потом практика, хотя обычно происходит все по-другому.
Tags:
Hubs:
-5
Comments7

Articles