Упаковка и распаковка
В теме о коллекциях вы встречали тип object. Переменной этого типа можно присвоить значение любого типа: число, строку, массив, объект вашего класса. object — ссылочный тип, его переменная хранит ссылку. Но число int живёт прямо в переменной, и ссылки на него нет. Как же оно попадает в object?
Представьте монету. Монета int лежит у вас в ладони, а переменная object умеет держать только ярлычок с адресом коробки. Поэтому C# кладёт копию монеты в коробку — маленький объект в куче — и отдаёт переменной object ссылку на эту коробку:
int coins = 5;
object box = coins;
coins = 7;
Console.WriteLine(box); // 5
int back = (int)box;
Console.WriteLine(back + 1); // 6Давайте разберём пример по строкам:
object box = coins;создаёт в куче коробку с копией числа5и кладёт вboxссылку на неё.coins = 7меняет только переменнуюcoins: в коробке лежит своя копия.Console.WriteLine(box)печатает5— содержимое коробки.(int)boxдостаёт из коробки копию числа и кладёт её вback.- Последняя строка печатает
6.
Не путайте эту коробку с коробкой из темы о типах: там коробкой была сама переменная. Здесь коробка — отдельный объект в куче, а переменная box держит только ярлычок с её адресом.
Здесь аналогия неточна. Настоящая монета в коробке — та же самая, что была у вас в руке. А C# кладёт в коробку копию, поэтому coins и коробка дальше живут отдельно.
Упаковка
Упаковка — это превращение значения значимого типа в объект в куче, например когда его присваивают переменной типа object. Она происходит сама: в строке object box = coins; нет ни одного лишнего слова.
Упаковка случается и там, где её не видно. Метод с параметром object принимает что угодно, и каждое число по дороге в него упаковывается:
Show(42); // 42
Show("hi"); // hi
Show(true); // True
void Show(object value)
{
Console.WriteLine(value);
}Разберём три вызова:
Show(42)упаковывает число42: параметруvalueнужна ссылка.Show("hi")ничего не упаковывает:stringи так ссылочный тип, и вvalueкопируется ссылка на строку.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:
object box = 42;
long big = (long)box; // ✗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.