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();
Или вот так:
// Ошибка компиляции: не указано required-свойство 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 нельзя применить к автоматическим полям или к чему-нибудь ещё кроме property.
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;
}
}
Стало на одно объявление переменной меньше. Чем код компактнее, тем лучше.
Почему важно иногда обращаться именно к полю внутри свойства?
- Иногда нужно явно контролировать где и как хранится значение (например, если вы хотите выдавать копию объекта, кэшировать результат или делать lazy-loading).
- Если вы хотите использовать атрибуты или reflection — поле может понадобиться по имени.
- В некоторых случаях (де)сериализации или performance-tunings вы хотите иметь больше контроля над хранением значения.
Варианты использования:
Валидация данных в сеттере
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;
}
}
Lazy Load
public string Data
{
get
{
if (field == null)
field = LoadDataFromDatabase();
return field;
}
set => field = value;
}
6. Типичные ошибки и нюансы
Ошибка №1: забыли инициализировать required-свойство.
Компилятор не даст пройти с таким кодом и выдаст ошибку сразу при сборке, что помогает избежать проблем во время работы программы.
Ошибка №2: required-свойства не работают без полной инициализации в конструкторе.
Если конструктор не принимает значения для всех обязательных свойств, компилятор напомнит, что вы что-то упустили.
Ошибка №3: попытка использовать required с const или readonly.
Эти модификаторы несовместимы — required можно применять только к обычным свойствам. Попытка их сочетать приведёт к ошибке.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ