1. Вступ
Програмувати на C# без властивостей можна, та це приблизно як кататися на роликах у Білому домі — можна, але незручно. Ми вже звикли до лаконічного синтаксису без зайвих гетерів і сетерів. А коли наш застосунок починає працювати з «серйозними» моделями (наприклад, класом User, що описує користувача в системі), важливо гарантувати, що всі потрібні дані справді задані.
Наприклад, якщо у нас є клас:
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
Легко забути ініціалізувати властивості:
User user = new User(); // Name буде null, Age = 0
У результаті десь за десять екранів і після сотні поворотів логіки ви отримаєте сумнозвісну NullReferenceException і довго шукатимете причину.
Нагадаємо про init-only властивості
Так, можна дозволити ініціалізацію лише під час створення:
public string Name { get; init; }
Але навіть із цим синтаксисом ніхто не змусить користувача класу передати значення, якщо він не захоче — конструктори за замовчуванням ініціалізують поля типовими значеннями (null, 0 тощо).
То як змусити розробника — або й себе — не забути задати потрібне значення? Саме для цього й придумали модифікатор required, який з’явився в C# 11.
2. required властивості: сувора ініціалізація
Модифікатор required — це вказівка компілятору стежити за тим, щоб конкретна властивість об’єкта була явно встановлена в момент його створення. Простіше кажучи, якщо таку властивість не ініціалізовано під час створення об’єкта — компілятор не пропустить, а IDE намалює страшну червону хвилю.
public class User
{
public required string Name { get; set; }
public int Age { get; set; }
}
Спробуймо створити користувача:
// Помилка компіляції: властивість Name є обов’язковою для ініціалізації!
User user1 = new User();
Або ось так:
// Помилка компіляції: не вказано обов’язкову властивість Name
User user2 = new User { Age = 18 };
А ось коректний спосіб:
User user3 = new User { Name = "Герміона", Age = 18 };
Як працює required?
Модифікатор required повідомляє компілятору: «Після завершення будь-якого конструктора об’єкта ця властивість має бути явно задана».
Це працює як для звичайних властивостей, так і для init-only:
public required string Name { get; init; }
Якщо ви визначаєте користувацький конструктор, який ініціалізує значення required-властивості, то правило теж дотримано.
Типова схема перевірки
| Сценарій | Компілятор задоволений? |
|---|---|
| Не задано required‑властивість | ❌ |
| Задано в об’єктному ініціалізаторі | ✔️ |
| Задано в конструкторі | ✔️ |
Приклад у нашій «собачій» моделі
public class Dog
{
public required string Name { get; set; }
public int Age { get; set; }
}
Dog dog = new Dog { Name = "Бобік", Age = 5 }; // Усе гаразд!
Dog badDog = new Dog { Age = 2 }; // Помилка! Не вказано Name.
Де реально допомагає required?
- Передача DTO між шарами: якщо у вас є API, важливо, щоб на вхід завжди надходили всі потрібні поля.
- Складні моделі з обов’язковими атрибутами: наприклад, Product з обов’язковим SKU, Order — з обов’язковим номером.
- На співбесідах і під час ревʼю: якщо ви показуєте такий синтаксис, ваш код, найімовірніше, читатимуть із повагою (і легкою заздрістю).
3. Як required працює з конструкторами?
Іноді ви явно описуєте конструктор. Що буде, якщо required-властивість не ініціалізувати в конструкторі або об’єктному ініціалізаторі? Компілятор видасть помилку.
public class Article
{
public required string Title { get; set; }
public required string Author { get; set; }
public Article()
{
// Якщо не ініціалізувати Title і Author — помилка компіляції!
// Можна так:
Title = "Без назви";
Author = "Невідомий";
}
}
Якщо конструктор сам присвоює значення required‑властивостям — усе чудово. Якщо ні, ви маєте ініціалізувати ці властивості через об’єктний ініціалізатор (new Article { ... }).
Специфіка використання
- required працює лише з властивостями, а не з полями.
- required не наслідується — якщо базова властивість позначена required, а в похідному класі ви не вказали required, компілятор не свариться (але гарною практикою буде явно повторити required у нащадку).
- required не можна застосувати до автоматичних полів або до чогось іще, окрім властивостей.
4. Стрілочний запис властивостей
З появою нових версій C# розробники все частіше прагнуть до лаконічного та виразного коду. Одне з найпомітніших нововведень для властивостей — стрілочний запис (або expression-bodied properties).
Іноді потрібно визначити властивість, яка просто повертає значення без будь-якої додаткової логіки. Раніше для цього доводилося писати повний ґеттер із фігурними дужками:
public int Age
{
get { return birthYear > 0 ? DateTime.Now.Year - birthYear : 0; }
}
Тепер можна писати набагато коротше — використовуючи стрілку (=>):
public int Age => birthYear > 0 ? DateTime.Now.Year - birthYear : 0;
Такий синтаксис називається властивістю з тілом-виразом (expression-bodied property). Він ідеально підходить для простих обчислень і робить код компактнішим.
Приклад використання get і set
public class Book
{
private string _title;
public string Title
{
get => _title;
set => _title = value.Trim();
}
}
Тут get повертає значення поля, а set — присвоює, попередньо прибираючи зайві пробіли на початку й у кінці.
Приклад використання тільки get (тільки для читання):
Якщо властивість тільки для читання, її можна записати взагалі без фігурних дужок, просто через =>.
public class Person
{
private string name = "Марк Твен";
// Тільки для читання: обчислювана властивість
public string Name => name.ToUpper();
}
Тут властивість Name можна тільки прочитати — вона завжди повертає name у верхньому регістрі.
5. Ключове слово field для властивостей
До C# 14, якщо ви хотіли звернутися до прихованого поля автоматичної властивості прямо в сеттері або ґеттері (наприклад, щоб захиститися від рекурсії або додати власну логіку), — це було неможливо. Доводилося оголошувати поле явно.
C# 14 дозволяє звертатися до прихованого поля автоматичної властивості за допомогою ключового слова field:
public class Person
{
public string Name
{
get => field; // field — це приховане поле властивості Name
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Імʼя не може бути порожнім!");
field = value; // використовуємо field замість явного _name
}
}
}
Раніше довелося б писати так:
private string _name;
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Імʼя не може бути порожнім!");
_name = value;
}
}
Стало на одне оголошення змінної менше. Чим код компактніший, тим краще.
Чому важливо іноді звертатися саме до поля всередині властивості?
- Іноді потрібно явно контролювати де й як зберігається значення (наприклад, якщо ви хочете повертати копію об’єкта, кешувати результат або робити ледаче завантаження).
- Якщо ви хочете використовувати атрибути або рефлексію — поле може знадобитися по імені.
- У деяких випадках (де)серіалізації або оптимізації продуктивності вам потрібно більше контролю над зберіганням значення.
Варіанти використання:
Валідація даних у сеттері
public double Grade
{
get => field;
set
{
if (value < 0 || value > 5)
throw new ArgumentOutOfRangeException("Оцінка має бути від 0 до 5");
field = value;
}
}
Статистика зміни значення
public int StepCount
{
get => field;
set
{
if (value > field)
{
Console.WriteLine($"Ура! Ви зробили {value - field} кроків більше!");
}
field = value;
}
}
Ледаче завантаження
public string Data
{
get
{
if (field == null)
field = LoadDataFromDatabase();
return field;
}
set => field = value;
}
6. Типові помилки та нюанси
Помилка № 1: забули ініціалізувати required-властивість.
Компілятор не пропустить такий код і видасть помилку одразу під час збирання, що допомагає уникнути проблем під час виконання програми.
Помилка № 2: required-властивості не працюють без повної ініціалізації в конструкторі.
Якщо конструктор не приймає значення для всіх обов’язкових властивостей, компілятор нагадає, що ви щось пропустили.
Помилка № 3: спроба використовувати required з const або readonly.
Ці модифікатори несумісні — required можна застосовувати лише до звичайних властивостей. Спроба їх поєднати призведе до помилки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ