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

Данная информация будет полезна тем, кто только разбирается с 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. Видимо разработчики реализовали коллекции так, что действительно, явное указание типа обобщения для переменной влияет на дальнейшее поведение объекта коллекции, особенно для значимых типов.

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

Вывод прост. Вначале теория, а потом практика, хотя обычно происходит все по-другому.
  • –5
  • 14.3k
  • 7
Share post

Similar posts

Comments 7

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

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

    Вы на сигнатуру IList.Add не смотрели, что ли? Там входной параметр — object, при приведении любого value type к object происходит боксинг.

    Все совершенно однозначно, что вас удивило?
      –2
      уточню: для «без боксинга» надо использовать IList<int>

      и вообще не надо включать пространство
      using System.Collections; // не генерики

      иначе такие косяки и всплывают. Для них лучше явно (UGLY) использовать
      System.Collections.IList l = new List<int>();

      по умолчанию в новых файлах (Add\New Class...) добавляется только пространство с генериками (VS2013+. ниже проверить не могу)
      using System.Collections.Generic;
        +2
        и вообще не надо включать пространство System.Collections иначе такие косяки и всплывают.

        То есть то, что сигнатуры классов различаются — не останавливает, что ли?
          –2
          если заюзать то пространство, то сигнатуры компилятором кушаются на ура: ты указал просто IList, он и будет использоваться
          если не юзать пространство с «негенериками», то при виде просто IList компилятор матюкнется, и избежим косяка
          Using the generic type 'System.Collections.Generic.IList' requires 1 type arguments
            +2
            Может просто не надо указывать типы, не подумав, какие именно нужны?
              –1
              может быть и вариант «хочу работать с коллекцией через интерфейсы»
              я комментил вариант «хочу работать с коллекцией через интерфейсы без боксинга»
              в общем, думаю, до него дошло в любых вариантах :)
                +2
                может быть и вариант «хочу работать с коллекцией через интерфейсы»

                Необходимости думать он не отменяет, даже наоборот.

    Only users with full accounts can post comments. Log in, please.