1. Введение
Позиционный синтаксис record-класса действительно удобен для простых случаев:
public record User(string Name, int Age);
Но иногда у вас появляется жгучее желание добавить к record дополнительные методы, нестандартные свойства, изменить модификаторы доступа, добавить логику в конструктор (например, валидацию или "автоматическое" преобразование данных). Увы, в позиционной записи это просто некуда добавить! Пора переходить к явному синтаксису тела, если:
- Вам нужно расширить record методами, свойствами или дополнительной логикой.
- Необходимо контролировать поведение свойств (сеттера, геттеры, inits, валидация).
- Требуется создать несколько разных конструкторов для разных способов инициализации.
- Нужно добавить интерфейсы или реализовать специальные методы.
Строим record с телом
Синтаксис очень похож на класс. Вам знакома эта форма:
public class User
{
/* ... */
}
Только теперь это record:
public record User
{
// Явно определенные свойства
public string Name { get; init; }
public int Age { get; init; }
// Дополнительная логика
public string GetGreeting()
{
return $"Привет, меня зовут {Name} и мне {Age} лет!";
}
// Кастомный конструктор
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("Age не может быть отрицательным!");
Age = age;
}
}
Забавный факт: Если вы определяете свои свойства явно, компилятор не создаёт для вас автоматически свойства из позиционного синтаксиса. Всё явно, всё под контролем.
Смешанный вариант: гибридный синтаксис
C# позволяет сочетать оба мира: вы можете объявить позиционный record и добавить к нему тело:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"Я {Name}, мне {Age} лет!";
}
}
В этом случае свойства Name и Age всё также автоматически создаются с помощью позиционного синтаксиса, а ваши дополнительные методы уютно располагаются внутри тела.
2. Отличия от обычных record — нюансы тела
- Явный record позволяет полностью контролировать конструкторы, свойства, методы.
- Вы можете реализовать интерфейсы или добавить собственную логику сравнения, если используются сложные правила идентификации объекта.
- В отличие от простого record с позиционным синтаксисом, для добавления новых свойств вам теперь достаточно просто объявить их в теле record’а.
// Ошибка новичков!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← конфликт! Повторное объявление свойства
}
Ошибки новичков: Некоторые студенты пытаются одновременно писать public record User(string Name, int Age) и добавлять в тело свойства public string Name { get; init; }, думая, что это две разные переменные. Нет! Это будет конфликт (повторное объявление). Либо используйте полностью позиционный синтаксис, либо явно определяйте свойства — не путайте.
Практические примеры
Мы продолжаем разрабатывать консольное приложение, в котором пользователь создает заказы. Пусть есть класс заказа Order, который до сих пор выглядел так:
public record Order(string Product, int Quantity, double Price);
Допустим, теперь нам нужна валидация количества (quantity не может быть меньше 1) и дополнительное свойство-счетчик общей стоимости:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("Количество должно быть не меньше 1!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"Товар: {Product}, Кол-во: {Quantity}, Итог: {TotalCost}";
}
Заметьте, что мы реализовали явно свойства, сделали их с init-сеттером (неизменяемые объекты), добавили автоматическое вычисление стоимости — код стал гибче!
Вызов в программе:
var order = new Order("Велосипед", 2, 15000);
Console.WriteLine(order); // Товар: Велосипед, Кол-во: 2, Итог: 30000
3. record struct
Эволюция struct-ов
До появления record struct структуры в C# были "простыми рабочими лошадками" — быстро копируемые, хранящиеся в стеке, отлично подходящие для коротких “Parcel-ов” данных (например, координат или цветов). Но они не поддерживали всех плюшек records: не было позиционного синтаксиса, with-выражений, сравнения по значению по умолчанию и прочих “сладких булочек”.
Теперь в C# можно объявлять структуры в стиле record:
public record struct Point(int X, int Y);
А что это вообще даёт?
- Автоматическая реализация Equals, GetHashCode, ToString — теперь ваша структура умеет красиво сравниваться и печататься!
- Синтаксис with-клонирования: var p2 = p1 with { X = 10 };
- Возможность использования позиционного или явного синтаксиса.
Сравнение: классический struct VS record struct
| struct | record struct | |
|---|---|---|
| Позиционный синтаксис | Нет | Да |
| with-выражение | Нет | Да |
| Неизменяемость | Нет (по умолч.) | Да (init) |
| Сравнение по значениям | Нет (по умолч.) | Да |
| ToString | Стандартный | Красивый |
Явный синтаксис тела для record struct
Всё так же, как у обычного record, только struct:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("Ширина > 0");
Height = height > 0 ? height : throw new ArgumentException("Высота > 0");
}
public void Print()
{
Console.WriteLine($"Размеры: {Width} x {Height}, площадь: {Area}");
}
}
Пример использования:
var rect = new Rectangle(10, 7);
rect.Print(); // Размеры: 10 x 7, площадь: 70
// Клонируем и изменяем ширину, не меняя исходник
var wideRect = rect with { Width = 20 };
wideRect.Print(); // Размеры: 20 x 7, площадь: 140
Особенности record struct
- Это всё ещё struct — value type. Копируется при присваивании!
- Все плюсы records: сравнение по значениям, with-клонирование, красивый ToString.
- Рекомендуется использовать для маленьких, компактных неизменяемых наборов данных, где важно избежать аллокаций в куче.
- Можно объявлять позиционные параметры или явно “раскрывать” тело.
4. Типичные ошибки и грабли
Излишняя мутабельность: record struct не делает поля автоматически неизменяемыми, если вы объявляете их как обычные (например, public int Value;). Используйте init-сеттеры для truly immutable struct!
Сравнение: Если вы добавляете новые поля вручную (не включив их в позиционный синтаксис), знайте: только те поля, что в конструкторе или с init-сеттером, участвуют в авто-сравнении по значению.
Копирование: Это struct, а значит... всё копируется! Не путайте с ссылочными record-айдами.
Путаница с with-выражениями: Они всегда создают shallow copy, то есть НЕ делают глубокое копирование вложенных объектов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ