Данная информация будет полезна тем, кто только разбирается с C#, на практике пытаясь понять теорию, не читая предварительно книжек до конца. Это скорее всего заметка на полях, а не подробное описание и анализ темы. Итак, кому интересно как ведут себя структуры в памяти при работе с коллекциями в C#, добро пожаловать.
Почитывая Рихтера по вечерам, решил более подробно разбираться в каждой детали, имея большое желание понять идеологию .Net глубже. Пролистывая страницы книги, пришел к выводу, что многое, что проектируется и пишется каждый день — постоянно нарушает некоторые основы .Net.
Рассмотрим на примере структур и коллекций. Не буду напоминать, что структуры это value-типы и ведут себя по-другому, нежели ссылочные.
Пример 1. Использование старого ArrayList.
Ожидаемым результатом будет наличие двух разных элементов в стеке типа System.Int32. Причем, один из них уже будет ссылочного типа. Если открыть IL-представление данного кода, то увидим справедливое подтверждение:
Операция box(boxing) создает объект в куче типа Int32. При добавлении элемента типа структур, каждый раз будет создаваться его аналог в куче, а ссылка попадать в коллекцию. На помощь этому действию пришли со временем generic-коллекции. Рассмотрим пример и его IL-представление:
Пример 2. Использование обобщений для коллекций:
Данный код преобразуется в:
Такого рода коллекции более совершенны и умеют работать с value-типами грамотнее, не используя операции box\unbox. Это значительно сокращает затраты памяти, увеличивает производительность и уменьшает количество сборок мусора.
Однако, поэкспериментировав с коллекциями некоторое время более детально, наткнулся на неожиданный момент. Иногда появляется потребность написать нечто в таком стиле как:
Код простой, но результат его для структур оказался неоднозначным.
Если мы посмотрим на IL-представление такого кода:
То получим следующее:
Мы опять попадаем на блок упаковки. И снова получим объект в куче как копию нашего значения myInt. Видимо разработчики реализовали коллекции так, что действительно, явное указание типа обобщения для переменной влияет на дальнейшее поведение объекта коллекции, особенно для значимых типов.
Это был первый вывод, которым попытался объяснить происходящее. В итоге объяснение нашлось чуть позже от самого автора. Любая интерфейсная переменная всегда должна быть размещена в куче, что и заставило появиться дополнительной операции упаковки.
Вывод прост. Вначале теория, а потом практика, хотя обычно происходит все по-другому.
Почитывая Рихтера по вечерам, решил более подробно разбираться в каждой детали, имея большое желание понять идеологию .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. Видимо разработчики реализовали коллекции так, что действительно, явное указание типа обобщения для переменной влияет на дальнейшее поведение объекта коллекции, особенно для значимых типов.
Это был первый вывод, которым попытался объяснить происходящее. В итоге объяснение нашлось чуть позже от самого автора. Любая интерфейсная переменная всегда должна быть размещена в куче, что и заставило появиться дополнительной операции упаковки.
Вывод прост. Вначале теория, а потом практика, хотя обычно происходит все по-другому.
lair
Вы на сигнатуру IList.Add не смотрели, что ли? Там входной параметр —
object
, при приведении любого value type кobject
происходит боксинг.Все совершенно однозначно, что вас удивило?
zelyony
уточню: для «без боксинга» надо использовать IList<int>
и вообще не надо включать пространство
иначе такие косяки и всплывают. Для них лучше явно (UGLY) использовать
по умолчанию в новых файлах (Add\New Class...) добавляется только пространство с генериками (VS2013+. ниже проверить не могу)
lair
То есть то, что сигнатуры классов различаются — не останавливает, что ли?
zelyony
если заюзать то пространство, то сигнатуры компилятором кушаются на ура: ты указал просто IList, он и будет использоваться
если не юзать пространство с «негенериками», то при виде просто IList компилятор матюкнется, и избежим косяка
lair
Может просто не надо указывать типы, не подумав, какие именно нужны?
zelyony
может быть и вариант «хочу работать с коллекцией через интерфейсы»
я комментил вариант «хочу работать с коллекцией через интерфейсы без боксинга»
в общем, думаю, до него дошло в любых вариантах :)
lair
Необходимости думать он не отменяет, даже наоборот.