Підрозділ 13.1
Відкладена ініціалізація та тип Lazy<T>
Пояснює відкладену ініціалізацію через Lazy<T>: створення об'єкта тільки в момент першого звернення до Value і економію пам'яті для не використаних залежностей.
13.1. Відкладена ініціалізація та тип `Lazy`
У реальних застосунках, зокрема медичних інформаційних системах, далеко не кожен об'єкт потрібний при кожному запуску програми. Наприклад, клас PatientRecord може містити посилання на об'єкт, що завантажує повну медичну картку пацієнта з бази даних — але лікар відкриває детальний вигляд лише для деяких пацієнтів. Якщо завантажувати картку завжди при створенні PatientRecord, програма даремно витрачатиме час і пам'ять на дані, які ніхто не переглядатиме.
Це фундаментальний компроміс: ініціалізувати заздалегідь (eager) означає завжди бути готовим, але платити вартість навіть за непотрібні об'єкти; ініціалізувати відкладено (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)); // trueIsValueCreated — властивість типу 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> для відкладеної побудови кешу нормативних значень аналізів: