Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created October 7, 2025 08:03
Show Gist options
  • Save sunmeat/935569451ed15bd3104a9ae9f10d8a67 to your computer and use it in GitHub Desktop.
Save sunmeat/935569451ed15bd3104a9ae9f10d8a67 to your computer and use it in GitHub Desktop.
упаковка та розпаковка C#
/*
Boxing — це перетворення value type (наприклад, int, struct) на reference type (object),
а unboxing — навпаки.
Це базова фішка C# для уніфікації типів, але зі своїми "підводними каменями".
Чому це важливо?
1. Продуктивність та пам'ять: Boxing створює об'єкт на heap, що викликає алокацію
(24+ байт на int) і додатковий тиск на Garbage Collector (GC).
У високонавантажених додатках (ігри, сервери) це призводить до пауз GC, лагів і витоків пам'яті.
Unboxing додає перевірки типів (може кинути InvalidCastException).
2. Типова безпека: Дозволяє value types працювати з колекціями як ArrayList
чи object-параметрами, - але без generics (List<T>) це "брудний" код.
3. У .NET 9 з'явилася нова фішка "Object Stack Allocation for Boxes":
Якщо boxed value type не "втікає" з методу (не зберігається в статичних/глобальних змінних), то алокується на stack, а не heap.
Це усуває алокації, зменшує GC на 20-50% у типових сценаріях (наприклад, Equals() на int).
Компілятор JIT робить це автоматично для 64-бітних додатків. .NET 8 — завжди heap, з алокаціями.
4. Масштабованість: У enterprise (ASP.NET, Unity) уникнення boxing — ключ до швидкості.
З generics (List<int>) boxing мінімізується, але legacy-код все ще страждає.
Без розуміння особливостей упаковки та розпаковки код стає повільним і непередбачуваним.
У .NET 9 це оптимізували, але все одно: порада уникати boxing, усюди де можна (та використовувати generics, ref structs).
*/
using System.Collections; // Для ArrayList (legacy з boxing)
using System.Diagnostics; // Для Stopwatch — вимірювання часу
namespace BoxingUnboxingSimpleDemo
{
/*
Struct — це value type. Boxing копіює весь вміст (X, Y) в object на heap (або stack у .NET 9).
structs більші за int, показують витрати.
*/
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
// це типовий сценарій, де boxing ховається в методах базового класу.
public override bool Equals(object? obj)
{
if (obj is Point p) // безпечний unboxing з 'is'
{
return X == p.X && Y == p.Y;
}
return false;
}
public override int GetHashCode() => X * 31 + Y;
}
internal class Program
{
// https://giannisakritidis.com/blog/Objects-On-Stack/
// статична змінна для "ескейпу": змушує зробити алокацію на heap в .NET 9
// якщо box "втікає" (зберігається поза методом), алокація буде на heap.
private static object? escapedBox;
static void Main()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
// 1: ПРОСТИЙ BOXING/UNBOXING INT
// int (stack) -> object (boxing) -> int (unboxing). У .NET 9 — все на stack, якщо локально.
Console.WriteLine("Крок 1: Boxing int...");
int num = 42; // Value type на stack
object boxedNum = num; // BOXING: Створює object
Console.WriteLine($"Оригінал: {num}");
Console.WriteLine($"Boxed: {boxedNum} (тип: {boxedNum.GetType().Name})");
int unboxedNum = (int)boxedNum; // UNBOXING: Каст з перевіркою
Console.WriteLine($"Unboxed: {unboxedNum}\n");
/* num (value type на stack) перетворюється на object (reference type).
CLR створює новий об'єкт на heap (традиційно), який містить копію значення 42 у своєму полі (типу int).
але не в .NET 9 (якщо нема ескейпа, а тут його нема).
boxedNum стає посиланням (reference) на цей об'єкт.
чому копіюється? бо value type — це "значення", а object — "посилання".
щоб уніфікувати (наприклад, для колекцій чи методів, що приймають object),
потрібно створити окрему сутність з копією. оригінал num не змінюється.
де лежить об'єкт після boxing у .NET 9?
якщо об'єкт не ескейпить з методу (тобто не зберігається в статичних змінних, полях класу, масивах чи не повертається з методу),
то в .NET 9 JIT-компілятор може алокувати його на stack замість heap.
це нова оптимізація "Object stack allocation for boxes" (escape analysis): об'єкт стає локальним,
автоматично очищається при виході з методу, без GC. boxedNum — локальна змінна, не ескейпить, тож на stack.
у .NET <= 8 — завжди на heap (алокація ~24+ байти на int, включаючи header об'єкта + sync block).
це призводить до тиску на GC. у .NET 9 — до 20-50% менше алокацій/пауз GC у типових сценаріях (наприклад, Equals на int).*/
// 2: BOXING STRUCT
// копіює весь struct. буде більше витрат, ніж з int.
Console.WriteLine("Крок 2: Boxing struct...");
Point pt = new Point(10, 20);
object boxedPt = pt; // BOXING struct
Console.WriteLine($"Оригінал: ({pt.X}, {pt.Y})");
Point unboxedPt = (Point)boxedPt; // UNBOXING
Console.WriteLine($"Unboxed: ({unboxedPt.X}, {unboxedPt.Y})");
bool equal = pt.Equals(boxedPt); // внутрішній boxing
Console.WriteLine($"Equals: {equal}\n");
// 3: ПРОДУКТИВНІСТЬ З STOPWATCH
// порівнюємо цикли з/без boxing. у .NET 9 — boxing швидший (stack).
Console.WriteLine("Крок 3: Продуктивність (1 млн ітерацій)...");
var swNoBox = Stopwatch.StartNew();
long sumNo = 0;
for (int i = 0; i < 1000000; i++)
{
sumNo += i; // без boxing
}
swNoBox.Stop();
Console.WriteLine($"Без boxing: {swNoBox.ElapsedMilliseconds} ms, Сума: {sumNo}");
var swBox = Stopwatch.StartNew();
long sumBox = 0;
object[] boxes = new object[1000000]; // підготовка для boxing
for (int i = 0; i < 1000000; i++)
{
boxes[i] = i; // BOXING
}
for (int i = 0; i < 1000000; i++)
{
sumBox += (int)boxes[i]; // UNBOXING
}
swBox.Stop();
Console.WriteLine($"З boxing: {swBox.ElapsedMilliseconds} ms, Сума: {sumBox}");
Console.WriteLine($"Різниця: {swBox.ElapsedMilliseconds - swNoBox.ElapsedMilliseconds} ms (менше в .NET 9)\n");
// 4: ПОМИЛКА UNBOXING
// InvalidCastException, якщо тип не співпадає. завжди перевіряємо!
Console.WriteLine("Крок 4: Помилка unboxing...");
object wrong = "рядок"; // не int
try
{
int bad = (int)wrong; // кине помилку
}
catch (InvalidCastException ex)
{
Console.WriteLine($"Помилка: {ex.Message} — перевір тип!\n");
}
// 5: КОЛЕКЦІЇ — ARRAYLIST VS LIST<INT>
// при використанні ArrayList — відбувається boxing кожного додавання. з List<int> — ні.
Console.WriteLine("Крок 5: Колекції...");
ArrayList arrList = new ArrayList();
var listInt = new List<int>();
var swArr = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
arrList.Add(i); // BOXING
}
swArr.Stop();
Console.WriteLine($"ArrayList (boxing): {swArr.ElapsedMilliseconds} ms");
var swList = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
listInt.Add(i); // без boxing
}
swList.Stop();
Console.WriteLine($"List<int> (без boxing): {swList.ElapsedMilliseconds} ms\n");
Console.WriteLine("=== ДЕМОНСТРАЦІЯ ЗАВЕРШЕНА. Натисніть клавішу. ===");
Console.ReadKey();
}
// МЕТОД COMPARE: без ескейпу — stack у .NET 9
// параметри object — boxing, але локально: stack alloc.
private static bool Compare(object? x, object? y)
{
return x?.Equals(y) ?? false;
}
// МЕТОД З ЕСКЕЙПОМ: Heap через статичну змінну
// escapedBox = x; — "втікає", змушує класти на heap.
private static bool CompareEscaped(object? a, object? b)
{
escapedBox = a; // ескейп!
return a?.Equals(b) ?? false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment