Значимые типы легче
ссылочных.
Для
Значимых типов них не нужно выделять память в управляемой куче, их не затрагивает сборка мусора и к ним нельзя обратиться через указатель.
Однако часто нужно получить ссылку на экземпляр значимого типа. Скажем, вы хотите сохранить структуры Point в объекте типа
ArrayList. В коде это выглядит примерно так:
C#
// Объявляем значимый тип.
struct Point
{
public int x, y;
}
public sealed class Program
{
public static void Main()
{
ArrayList a = new ArrayList();
Point p; // Выделяется память для Point (не в куче).
for (Int32 i = 0; i < 10; i++)
{
p.x = p.y = i; // Инициализация членов в нашем значимом типе.
a.Add(p); // Упаковка значимого типа и добавление
// ссылки в ArrayList.
}
...
}
}
C#
public virtual Int32 Add(Object value);
Отсюда видно, что в качестве параметра
Add нужен
Object, то есть ссылка (или указатель) на объект в управляемой куче.
Но в примере я передаю переменную
p, имеющую значимый тип
Point. Чтобы код работал, нужно преобразовать значимый тип
Point в
объект из
управляемой кучи и получить на него
ссылку.
Преобразование значимого типа в ссылочный позволяет выполнить упаковка (boxing).
При упаковке экземпляра значимого типа происходит следующее:
1. В управляемой куче выделяется память. Ее объем определяется длиной значимого типа и двумя дополнительными членами, необходимыми для всех объектов в управляемой куче, — указателем на объект-тип и индексом SyncBlockIndex.
2. Поля значимого типа копируются в память, выделенную только что в куче.
3. Возвращается адрес объекта. Этот адрес является ссылкой на объект; значимый тип превратился в ссылочный.
Некоторые компиляторы, например компилятор C#, автоматически создают IL код, необходимый
для упаковки экземпляра значимого типа, но вы должны понимать, что происходит «за кулисами», и помнить об опасности «распухания» кода и снижения производительности.
В предыдущем примере компилятор C# обнаружил, что методу, требующему
ссылочный тип, я передаю как параметр
значимый тип, и автоматически создал
код для упаковки объекта.
Вследствие этого поля экземпляра p значимого типа Point в период выполнения скопируются во вновь созданный в куче объект Point. Полученный адрес упакованного объекта Point (теперь это ссылочный тип) будет передан методу
Add. Объект P
oint останется в
куче до очередной сборки мусора.
Переменную
p значимого типа
Point можно повторно использовать или удалить из памяти, так как
ArrayList ничего о ней не знает.
Заметьте: время жизни упакованного значимого типа превышает время жизни неупакованного значимого типа.
Многие языки, разработанные для
CLR (например,
C# и Visual Basic), автоматически создают код для
упаковки значимых типов в ссылочные, когда это необходимо. Однако другие языки (вроде
C++ с Managed Extensions) требуют, чтобы программист сам писал код упаковки значимых типов там, где это требуется.
Здесь ссылку (или указатель), содержащуюся в элементе с номером 0 массива
ArrayList, вы пытаетесь поместить в переменную
p значимого типа
Point.
Для этого все поля, содержащиеся в упакованном объекте
Point, надо скопировать в переменную
p значимого типа, находящуюся в стеке потока.
CLR выполняет эту процедуру в два этапа.
1) Сначала извлекается адрес полей Point из упакованного объекта Point. Этот процесс называют распаковкой (unboxing).
2) Затем значения полей копируются из кучи в экземпляр значимого типа, находящийся в стеке.
Распаковка не является точной противоположностью
упаковки. Она гораздо менее ресурсозатратна, чем упаковка, и состоит только в получении указателя на исходный значимый тип (поля данных), содержащийся в объекте. В сущности, указатель ссылается на неупакованную часть упакованного экземпляра. И никакого копирования при распаковке (в отличие от упаковки). Однако обычно вслед за распаковкой выполняется копирование полей, поэтому в сумме обе эти опера ции являются отражением операции упаковки.
Понятно, что упаковка и распаковка/копирование снижают производительность приложения (как в плане замедления, так и дополнительной памяти), поэтому нужно знать, когда компилятор сам создает код для выполнения этих операций, и стараться минимизировать создание такого кода.
При распаковке ссылочного типа происходит следующее:
1. Если переменная, содержащая ссылку на упакованный значимый тип, равна null,
генерируется исключение NullReferenceException.
2. Если ссылка указывает на объект, не являющийся упакованным значением тре
буемого значимого типа, генерируется исключение InvalidCastException.
Иллюстрацией второго пункта может быть код, который не работает так, как
хотелось бы:
C#
public static void Main()
{
Int32 x = 5;
Object o = x; // Упаковка x; o указывает на упакованный объект.
Int16 y = (Int16) o; // Генерируется InvalidCastException.
}
Казалось бы, можно взять упакованный экземпляр Int32, на который указывает o, и привести к типу Int16. Но, когда выполняется распаковка объекта, должно быть сделано приведение к неупакованному типу (в нашем случае, к Int32). Вот как выглядит исправленный вариант:
C#
public static void Main()
{
Int32 x = 5;
Object o = x; // Упаковка x; o указывает на упакованный объект.
Int16 y = (Int16)(Int32) o; // Распаковка, а затем приведение типа.
}
Как я уже говорил, распаковка часто сопровождается копированием полей. Код на C# демонстрирует, что операции распаковки и копирования всегда работают совместно:
C#
public static void Main()
{
Point p;
p.x = p.y = 1;
Object o = p; // Упаковка p; o указывает на упакованный объект.
p = (Point) o; // Распаковка o И копирование полей из экземпляра в стек.
}
В последней строке компилятор C# генерирует IL команду для распаковки o (получение адреса полей в упакованном экземпляре) и еще одну ILкоманду для копирования полей из кучи в переменную p, располагающуюся в стеке.
CLR также позволяет распаковывать значимые типы в версию этого же типа, поддерживающую присвоение
null.
Теперь такой пример:
C#
public static void Main()
{
Point p;
p.x = p.y = 1;
Object o = p; // Упаковка p; o указывает на упакованный экземпляр.
// Изменение поля x структуры Point (присвоение числа 2).
p = (Point) o; // Распаковка o И копирование полей из экземпляра
// в переменную в стеке.
p.x = 2; // Изменение состояния переменной в стеке.
o = p; // Упаковка p; o ссылается на новый упакованный экземпляр.
}
Во второй части примера нужно лишь изменить поле x структуры Point с 1 на 2.
Для этого выполняют распаковку, копирование полей, изменение поля (в стеке) и упаковку (создающую новый объект в управляемой куче). Надеюсь, вы понимаете, что все эти операции не могут не сказаться на производительности приложения.
Вместо
ArrayList следует использовать класс
List<T>.
Обобщенные классы коллекций (List<T>, ...) во многих отношениях совершеннее своих
необобщенных коллекций (ArrayList, ...).
Одно из примуществ
обобщенных классов возможности работать с наборами
значимых типов,
не прибегая к их упаковке/распаковке.
Эта особенность позволяет значительно повысить производительность, так как значительно сокращается число создаваемых в управляемой куче объектов, что, в свою очередь, сокращает число проходов сборщика мусора в приложении.
В результате обеспечивается безопасность типов на этапе компилирования, а код становится понятнее за счет сокращения числа приведений типов.