OOP Course
Сьогодні

Підрозділ 13.1

Відкладена ініціалізація та тип Lazy<T>

Пояснює відкладену ініціалізацію через Lazy<T>: створення об'єкта тільки в момент першого звернення до Value і економію пам'яті для не використаних залежностей.

13.1. Відкладена ініціалізація та тип `Lazy`

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

Це фундаментальний компроміс: ініціалізувати заздалегідь (eager) означає завжди бути готовим, але платити вартість навіть за непотрібні об'єкти; ініціалізувати відкладено (lazy) означає нести витрати тільки тоді, коли об'єкт справді потрібен.

Lazy<T> — відкладена ініціалізація: Eager vs Lazy

Проблема eager-ініціалізації

Розглянемо клас читача, який зберігає посилання на бібліотеку — об'єкт, що займає значну кількість пам'яті:

class Reader
{
    Library library = new Library(); // створюється ЗАВЖДИ при new Reader()

    public void ReadBook()
    {
        library.GetBook();
        Console.WriteLine("Читаємо паперову книгу");
    }

    public void ReadEbook()
    {
        Console.WriteLine("Читаємо книгу на комп'ютері");
    }
}

class Library
{
    private string[] books = new string[99]; // займає пам'ять

    public void GetBook()
    {
        Console.WriteLine("Видаємо книгу читачеві");
    }
}

Якщо читач використовує лише електронні книги:

Reader reader = new Reader();
reader.ReadEbook(); // library нікуди не використовується, але вже в пам'яті

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

Клас `Lazy` — відкладений обгортувач

Для вирішення цієї проблеми .NET надає узагальнений клас Lazy<T>. Він є легким обгортувачем (wrapper): сам по собі Lazy<T> займає мінімум пам'яті і не створює об'єкт T до першого звернення через властивість Value.

Перепишемо клас читача:

class Reader
{
    Lazy<Library> library = new Lazy<Library>();

    public void ReadBook()
    {
        library.Value.GetBook(); // .Value — перший виклик створює Library
        Console.WriteLine("Читаємо паперову книгу");
    }

    public void ReadEbook()
    {
        Console.WriteLine("Читаємо книгу на комп'ютері");
    }
}

Тепер при new Reader() об'єкт Library не створюється — лише легкий Lazy<Library>. Об'єкт Library з'явиться в heap тільки при першому виклику library.Value, тобто тільки якщо хтось викликав ReadBook().

Конструктори Lazy

Lazy<T>() без параметрів вимагає, щоб T мав публічний конструктор без параметрів — він буде викликаний при першому зверненні до Value:

Lazy<Library> library = new Lazy<Library>(); // new Library() відкладено

Якщо потрібна фабрична функція (конструктор з параметрами або складна ініціалізація), передається лямбда:

Lazy<PatientHistory> history = new Lazy<PatientHistory>(
    () => new PatientHistory(patientId, connectionString));

Це найпоширеніший варіант у реальному коді: завдяки замиканню лямбда може захопити будь-які необхідні аргументи, доступні на момент оголошення поля.

Властивості Value та IsValueCreated

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

Lazy<Library> library = new Lazy<Library>();

// Перше звернення — тригер ініціалізації
var lib1 = library.Value; // new Library() викликається тут

// Наступні звернення — кеш, без повторного виклику конструктора
var lib2 = library.Value; // той самий об'єкт, що й lib1
Console.WriteLine(object.ReferenceEquals(lib1, lib2)); // true

IsValueCreated — властивість типу bool, яка дозволяє перевірити стан без запуску ініціалізації:

Lazy<Library> library = new Lazy<Library>();

Console.WriteLine(library.IsValueCreated); // false — ще не створено
var lib = library.Value;                   // ініціалізація тут
Console.WriteLine(library.IsValueCreated); // true

Це корисно в сценаріях, де потрібно з'ясувати, чи вже виконувалася дорога операція — наприклад, для виведення стану кешу в діагностиці або логуванні.

Потокова безпека: LazyThreadSafetyMode

Третій параметр конструктора Lazy<T> — режим потокової безпеки:

// За замовчуванням: ExecutionAndPublication
Lazy<Service> s1 = new Lazy<Service>(
    () => new Service(),
    LazyThreadSafetyMode.ExecutionAndPublication);

// Без синхронізації (однопотоковий код)
Lazy<Service> s2 = new Lazy<Service>(
    () => new Service(),
    LazyThreadSafetyMode.None);
Режим Поведінка Коли обирати
ExecutionAndPublication Тільки один потік виконує ініціалізацію; решта чекають За замовчуванням, якщо є ризик конкурентного доступу
PublicationOnly Кілька потоків можуть ініціалізувати паралельно; збережеться перший результат Якщо ініціалізація ідемпотентна і без побічних ефектів
None Жодної синхронізації Тільки для однопотокових сценаріїв

У типових додатках без явного багатопотоку (наприклад, у веб-запитах, де кожен запит має свій потік) достатньо значення за замовчуванням.

Медична картка з відкладеним завантаженням — runnable приклад

Демонструємо IsValueCreated та момент ініціалізації:

Сервіс нормативів — runnable приклад

Lazy<T> для відкладеної побудови кешу нормативних значень аналізів:

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