Перейти к содержимому
Теория · Значимые и ссылочные типы · урок 5 из 11

Упаковка и распаковка

В теме о коллекциях вы встречали тип object. Переменной этого типа можно присвоить значение любого типа: число, строку, массив, объект вашего класса. object — ссылочный тип, его переменная хранит ссылку. Но число int живёт прямо в переменной, и ссылки на него нет. Как же оно попадает в object?

Представьте монету. Монета int лежит у вас в ладони, а переменная object умеет держать только ярлычок с адресом коробки. Поэтому C# кладёт копию монеты в коробку — маленький объект в куче — и отдаёт переменной object ссылку на эту коробку:

C#
int coins = 5;
object box = coins;
coins = 7;
Console.WriteLine(box); // 5
int back = (int)box;
Console.WriteLine(back + 1); // 6

Давайте разберём пример по строкам:

  1. object box = coins; создаёт в куче коробку с копией числа 5 и кладёт в box ссылку на неё.
  2. coins = 7 меняет только переменную coins: в коробке лежит своя копия.
  3. Console.WriteLine(box) печатает 5 — содержимое коробки.
  4. (int)box достаёт из коробки копию числа и кладёт её в back.
  5. Последняя строка печатает 6.

Не путайте эту коробку с коробкой из темы о типах: там коробкой была сама переменная. Здесь коробка — отдельный объект в куче, а переменная box держит только ярлычок с её адресом.

Здесь аналогия неточна. Настоящая монета в коробке — та же самая, что была у вас в руке. А C# кладёт в коробку копию, поэтому coins и коробка дальше живут отдельно.

Упаковка

Упаковка — это превращение значения значимого типа в объект в куче, например когда его присваивают переменной типа object. Она происходит сама: в строке object box = coins; нет ни одного лишнего слова.

Упаковка случается и там, где её не видно. Метод с параметром object принимает что угодно, и каждое число по дороге в него упаковывается:

C#
Show(42);   // 42
Show("hi"); // hi
Show(true); // True

void Show(object value)
{
    Console.WriteLine(value);
}

Разберём три вызова:

  1. Show(42) упаковывает число 42: параметру value нужна ссылка.
  2. Show("hi") ничего не упаковывает: string и так ссылочный тип, и в value копируется ссылка на строку.
  3. Show(true) упаковывает bool — тоже значимый тип.

Распаковка

Распаковка — обратный шаг: приведение (int)box проверяет, что в коробке лежит int, и копирует число из неё. Распаковку всегда пишут явно. Строка int back = box; не скомпилируется: компилятор не знает, что лежит в object, и требует приведения (ошибка CS0266).

Распаковка открывает коробку тем типом, который в ней лежит. Упакованный int достают как int, а не как long или double; упакованный bool — как bool.

Зачем знать об упаковке

Каждая упаковка создаёт в куче новый объект. Одна коробка незаметна, но миллион упаковок в цикле — это миллион объектов, которые потом убирает сборщик мусора.

Теперь видно, что происходило в ArrayList из темы о коллекциях. Он хранит элементы как object, поэтому каждое число в нём упаковано, а при чтении его распаковывают приведением (int). List<int> хранит сами числа и обходится без коробок.

Этот вопрос часто задают на собеседовании: что такое упаковка и чем она плоха? Ответ из двух частей:

  • упаковка кладёт копию значения значимого типа в новый объект в куче, а распаковка явным приведением копирует значение обратно;
  • каждая коробка — лишний объект в куче, поэтому в часто выполняемом коде упаковки избегают: например, берут List<int>, а не ArrayList.

Частая ошибка

Упакованный int попробовали распаковать сразу в long:

C#
object box = 42;
long big = (long)box; // ✗
TEXT
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Int64'.

Программа компилируется, но при запуске останавливается на второй строке. System.Int32 и System.Int64 в сообщении — полные имена типов int и long в .NET.

Обычно int превращается в long без всякого приведения. Но распаковка — не преобразование числа: в box лежит int, и (long)box не может его открыть. Сначала распакуйте в int, а затем расширьте до long: long big = (int)box;.

Главное

  • Упаковка кладёт копию значения в объект в куче, когда значимый тип присваивают переменной object; она происходит сама.
  • Распаковку пишут явно, (int)box, и она копирует значение из коробки.
  • Распаковывают в тот тип, что лежит в коробке: (long) для упакованного int выбрасывает InvalidCastException.
  • Каждая упаковка создаёт объект в куче, поэтому для чисел List<int> лучше, чем ArrayList.