1. Вступ
Позиційний синтаксис record-класу дуже зручний для простих випадків:
public record User(string Name, int Age);
Але іноді у вас з’являється сильне бажання додати до record ще якісь методи, нетипові властивості, змінити модифікатори доступу, додати логіку до конструктора (наприклад, валідацію або «автоматичне» перетворення даних). На жаль, у позиційному записі цьому просто ніде бути! Час переходити до явного синтаксису тіла, якщо:
- Вам потрібно розширити record методами, властивостями або додатковою логікою.
- Потрібно контролювати поведінку властивостей (сетери, ґетери, init, валідація).
- Потрібно створити кілька різних конструкторів для різних способів ініціалізації.
- Треба додати інтерфейси або реалізувати спеціальні методи.
Будуємо 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# були «простими робочими конячками» — швидко копіювалися, зберігалися у стеку, чудово підходили для коротких наборів даних (наприклад, координат або кольорів). Але вони не підтримували всіх можливостей 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 для по-справжньому незмінного struct!
Порівняння: якщо ви додаєте нові поля вручну (не включаючи їх у позиційний синтаксис), майте на увазі: лише ті поля, які в позиційному конструкторі або з сетером init, беруть участь в автоматичному порівнянні за значенням.
Копіювання: це struct, а отже… усе копіюється! Не плутайте з посилальними record-ами.
Плутанина з with-виразами: вони завжди створюють поверхневу копію (shallow copy), тобто не виконують глибокого копіювання вкладених об’єктів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ