OOP Course
Сьогодні

Підрозділ 3.5

Типи значень та типи посилань

Пояснює різницю між типами значень і типами посилань, стеком і купою, копіюванням структур та поведінкою посилальних полів.

3.5. Типи значень та типи посилань

У попередніх розділах ми розглядали різні типи даних C#: прості (int, double, bool), а також складені — структури та класи. Усі ці типи поділяються на дві фундаментальні категорії: типи значень (value types) і типи посилань (reference types). Розуміння цієї різниці є одним із ключових у мові C#, оскільки вона визначає поведінку при копіюванні, передачі у методи та роботі з пам'яттю.

Типи значень:

  • Цілочисленні типи: byte, sbyte, short, ushort, int, uint, long, ulong
  • Типи з плаваючою комою: float, double
  • Тип decimal
  • Тип bool
  • Тип char
  • Перерахування enum
  • Структури struct

Типи посилань:

  • Тип object
  • Тип string
  • Класи class
  • Інтерфейси interface
  • Делегати delegate

Організація пам'яті: стек і купа

Щоб зрозуміти різницю між цими двома категоріями, необхідно розібратися з тим, як .NET організовує пам'ять. Середовище виконання використовує два принципово різних регіони пам'яті: стек (stack) і купу (heap).

Стек — це область пам'яті, що працює за принципом «останній зайшов — перший вийшов». Кожен виклик методу виділяє у стеку окремий кадр (stack frame), де зберігаються локальні змінні та параметри цього методу. Коли метод завершується, його кадр автоматично звільняється. Стек дуже швидкий, але обмежений за розміром.

Купа (heap) — невпорядкований набір об'єктів різного розміру. Тут зберігаються дані об'єктів посилального типу. Час життя об'єктів у купі не прив'язаний до конкретного методу — вони живуть доти, доки на них існують посилання. Коли посилань більше немає, збирач сміття (garbage collector) автоматично звільняє зайняту пам'ять.

Ось як виглядає розміщення при роботі з методом, де є локальні змінні:

static void RegisterVisit(int patientId)
{
    int roomNumber = 12;
    int duration   = 30;
    int totalCost  = duration * patientId;
}

При виклику RegisterVisit у стеку виділяється кадр з трьома змінними: patientId, roomNumber, duration і totalCost — усі типу int, тобто типи значень. По завершенні методу кадр зникає, а пам'ять повторно використовується.

Struct у стеку, клас у купі

Ключова відмінність між структурою і класом у тому, де зберігаються їх дані. Коли оголошується змінна типу структури, її поля розміщуються безпосередньо у стеку. Коли оголошується змінна типу класу, у стеку зберігається лише посилання (адреса), а самі дані об'єкта розміщуються у купі:

Розміщення struct та class у пам'яті: стек та купа

Коли об'єкт Patient більше не потрібний (на нього немає жодного посилання), збирач сміття автоматично звільняє пам'ять у купі. Зі стеком цього не потрібно — кадр методу зникає сам по собі.

Копіювання значень

Тип даних суттєво впливає на те, що саме відбувається при присвоєнні однієї змінної іншій.

При присвоєнні типу значень створюється повна незалежна копія даних. Подальші зміни однієї змінної не впливають на іншу:

При присвоєнні типу посилань копіюється лише посилання — обидві змінні вказують на той самий об'єкт у купі. Зміна через одну змінну відразу видна через іншу:

Це пояснює, чому в попередньому розділі зміна bp2.systolic не торкалась bp1 — там структура. А тут зміна patient2.name одразу відображається в patient1 — бо обидві змінні вказують на один і той самий об'єкт у купі.

Типи посилань всередині типів значень

Окремо варто розглянути ситуацію, коли структура містить поле посилального типу. Це тонкий момент, де легко допуститися помилки. Розглянемо структуру Visit (візит пацієнта), яка містить поле типу класу Diagnosis:

Що тут відбувається? При присвоєнні visit2 = visit1 структура справді копіюється — але лише поверхово (shallow copy). Це означає, що поле date (рядок) копіюється як значення, а поле diagnosis — це посилання на об'єкт у купі, і копіюється лише це посилання, а не сам об'єкт. Тому visit1.diagnosis і visit2.diagnosis вказують на той самий об'єкт у купі, і зміна через одну структуру впливає на іншу.

Дві структури з посиланням на один об'єкт у купі

Це типове джерело помилок при роботі зі структурами, що містять поля-класи. Якщо потрібна справжня незалежна копія, необхідно явно створювати новий об'єкт вкладеного класу — або взагалі переосмислити архітектуру і замінити клас структурою там, де це доречно.

Boxing та Unboxing

У C# всі типи — і значень, і посилань — наслідують від базового типу object. Це дозволяє покласти, наприклад, int у змінну типу object. Але оскільки int — тип значень (стек), а object — тип посилань (купа), відбувається boxing (упаковка): значення копіюється зі стека в купу, де для нього виділяється новий об'єкт-обгортка.

Зворотна операція — unboxing (розпаковка): явне приведення object назад до конкретного типу значень, при якому значення копіюється з купи назад у стек.

На перший погляд — зручно. Але boxing і unboxing мають реальну ціну: кожен boxing — це виділення пам'яті в купі і копіювання даних, а кожен unboxing — ще одне копіювання і перевірка типу. У гарячих ділянках коду (цикли з тисячами ітерацій, масові колекції) це може суттєво знизити продуктивність.

Якщо тип виконання об'єкта не збігається з типом приведення — виникне виняток InvalidCastException. Наприклад, (double)boxedInt впаде, навіть якщо числово int і double сумісні — потрібно спочатку розпакувати в int, а потім перетворити.

Boxing та Unboxing — переміщення між стеком і купою

Саме для уникнення boxing у C# з'явились узагальнення (generics) — наприклад, List<int> зберігає int безпосередньо без упаковки, на відміну від старого ArrayList, який приймав object і боксував кожне значення. Детально generics розглядаються у наступних розділах.

Розроблено Tomka Yurii · © 2026 ·