Created
October 7, 2025 08:03
-
-
Save sunmeat/935569451ed15bd3104a9ae9f10d8a67 to your computer and use it in GitHub Desktop.
упаковка та розпаковка C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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