Давайте представим, что вам нужно создать класс для хранения данных пользователя. Сколько строк кода вы напишете? Конструктор, свойства, метод ToString(), сравнение объектов... А если добавится новое поле? Придется обновлять конструктор, метод Equals, GetHashCode — утомительная работа, которая не добавляет бизнес‑ценности вашему приложению.
Records — это один из самых мощных и недооцененных новичками инструментов в C#. Он автоматически генерирует за вас тонны бойлерплейт‑кода (это повторяющийся программный код, который программисту приходится писать из‑за требований языка программирования, операционной системы, библиотеки подпрограмм и прочего), который раньше приходилось писать вручную. С приходом C# 9 и C# 10 работа с данными (DTO, Value Objects, модели) стала на порядок проще и безопаснее.
В этой статье мы разберем, как records меняют подход к написанию кода, и почему они должны стать вашим стандартным выбором для представления данных.
Чем record class отличается от class?
Начнем с самого простого примера. Вот как выглядит обычный класс:
// Старый подход: много кода для простых данных public class User { public string FirstName { get; init; } public string LastName { get; init; } public int Age { get; init; } public User(string firstName, string lastName, int age) { FirstName = firstName; LastName = lastName; Age = age; } }
А вот как это же выглядит с record:
// Новый подход: одна строка! public record User(string FirstName, string LastName, int Age);
Вот и все! Компилятор автоматически создает все необходимое: конструктор с параметрами, свойства FirstName, LastName, Age и многое другое. Как видно, удобство на лицо. Теперь давайте разберемся с тем, как все это работает.
Семантика
Начнем с того, что класс — это ссылочный тип с семантикой идентичности. Два объекта класса считаются равными, если это один и тот же объект в памяти (или если вы переопределили Equals).
Record — это ссылочный тип с семантикой значения. Два record‑объекта считаются равными, если их данные совпадают.
Посмотрим пример:
var user1 = new User("Иван", "Петров", 30); var user2 = new User("Иван", "Петров", 30); Console.WriteLine(user1 == user2); // True! (даже если это разные объекты) Console.WriteLine(ReferenceEquals(user1, user2)); // False
Теперь давайте посмотрим на примере консольного приложения, что на самом деле генерирует компилятор по шагам.
// Определяем record public record Person(string Name, string Email, int Age); class Program { static void Main() { // Создаем экземпляры var person1 = new Person("Анна Смирнова", "anna@example.com", 28); var person2 = new Person("Анна Смирнова", "anna@example.com", 28); var person3 = new Person("Иван Петров", "ivan@example.com", 35); // 1. Автоматический ToString() Console.WriteLine("=== ToString() ==="); Console.WriteLine(person1); // Вывод: Person { Name = Анна Смирнова, Email = anna@example.com, Age = 28 } // 2. Сравнение по значениям Console.WriteLine("\n=== Сравнение ==="); Console.WriteLine($"person1 == person2: {person1 == person2}"); // True Console.WriteLine($"person1 == person3: {person1 == person3}"); // False // 3. Автоматическая реализация IEquatable<T> Console.WriteLine($"\n=== Equals ==="); Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // True // 4. Получение хеш-кода на основе данных Console.WriteLine($"\n=== GetHashCode() ==="); Console.WriteLine($"person1 Hash: {person1.GetHashCode()}"); Console.WriteLine($"person2 Hash: {person2.GetHashCode()}"); Console.WriteLine($"person3 Hash: {person3.GetHashCode()}"); // 5. Использование в коллекциях (работает как Value Object) Console.WriteLine($"\n=== Работа со словарем ==="); var dictionary = new Dictionary<Person, string>(); dictionary[person1] = "Первый пользователь"; // Поиск по значению, а не по ссылке! Console.WriteLine($"Поиск person2: {dictionary[person2]}"); // "Первый пользователь" // 6. Деконструкция (разбор на составляющие) Console.WriteLine($"\n=== Деконструкция ==="); var (name, email, age) = person1; Console.WriteLine($"Имя: {name}, Email: {email}, Возраст: {age}"); } }
Запустите этот код и вы увидите, как много функциональности работает «из коробки» без единой дополнительной строки кода.
Иммутабельное копирование
Records по умолчанию иммутабельны, то есть неизменяемы. Это огромное преимущество для многопоточных приложений и для предотвращения случайных изменений данных. Но как создать копию с небольшими изменениями? Для этого существуют with‑выражения.
public record Person(string Name, string Email, int Age); class Program { static void Main() { var original = new Person("Анна Смирнова", "anna@example.com", 28); // Создаем копию с новым возрастом var older = original with { Age = 30 }; // Создаем копию с новым именем и email var renamed = original with { Name = "Анна Петрова", Email = "anna.petrova@example.com" }; Console.WriteLine($"Оригинал: {original}"); Console.WriteLine($"Старше: {older}"); Console.WriteLine($"Переименован: {renamed}"); // Исходный объект не изменился! Console.WriteLine($"\nОригинал не изменился: {original}"); } }
При этом важно понимать, что with‑выражения создают глубокую копию только для свойств значимых типов и record‑типов. Для обычных классов (ссылочных типов) копируется только ссылка.
Посмотрим, как это работает с вложенными records.
public record Address(string City, string Street); public record Employee(string Name, Address Address); class Program { static void Main() { var original = new Employee( "Иван", new Address("Москва", "Тверская") ); // Меняем только улицу во вложенном объекте var updated = original with { Address = original.Address with { Street = "Новый Арбат" } }; Console.WriteLine($"Оригинал: {original}"); // Employee { Name = Иван, Address = Address { City = Москва, Street = Тверская } } Console.WriteLine($"Обновлен: {updated}"); // Employee { Name = Иван, Address = Address { City = Москва, Street = Новый Арбат } } } }
Record struct вместо класса
C# 10 добавил возможность создавать records как структуры. Это важно для сценариев, где критична производительность и где объекты маленькие и создаются часто.
Вот пример record struct
// Record struct — значимый тип public record struct Point(int X, int Y); // Record class — ссылочный тип public record class Rectangle(Point TopLeft, Point BottomRight); class Program { static void Main() { // Record struct копируется по значению var point1 = new Point(10, 20); var point2 = point1; // Создается копия! point2 = point2 with { X = 30 }; Console.WriteLine($"Point1: {point1}"); // Point { X = 10, Y = 20 } Console.WriteLine($"Point2: {point2}"); // Point { X = 30, Y = 20 } // Record class копируется по ссылке var rect1 = new Rectangle(new Point(0, 0), new Point(100, 100)); var rect2 = rect1; // Копируется ссылка, объект один Console.WriteLine($"\nrect1 == rect2: {rect1 == rect2}"); // True (один объект) Console.WriteLine($"ReferenceEquals(rect1, rect2): {ReferenceEquals(rect1, rect2)}"); // True } }
Соответственно, возникает вопрос, когда что использовать? Используйте record class, когда создаете DTO для передачи данных между слоями, работаете с ORM (Object‑Relational Mapping), вам нужно наследование или объекты относительно крупные.
А record struct используйте, когда объект маленький (до 16–24 байт), создается много экземпляров (сотни тысяч), нужна максимальная производительность или объект представляет простую пару координат, диапазон и так далее
Расширенное использование: позиционные vs непозиционные records
Теперь давайте разберемся с еще одним важным моментом: позиционными и непозиционными records. Вот пример позиционного record с параметрами конструктора:
// Все свойства генерируются автоматически public record Product(string Name, decimal Price, int Quantity);
А непозиционный record с явным определением свойств будет иметь следующий вид:
// Более гибкий вариант public record Order { public int Id { get; init; } public DateTime CreatedAt { get; init; } public List<Product> Items { get; init; } = new(); // Можно добавлять свои методы public decimal GetTotal() => Items.Sum(p => p.Price * p.Quantity); // Можно переопределять стандартные методы public override string ToString() => $"Order #{Id} from {CreatedAt:d}"; }
Таким образом, позиционный record это лаконичный способ объявления, где компилятор автоматически генерирует первичный конструктор, свойства init, методы ToString(), Equals(), GetHashCode() и деконструкцию — идеально подходит для простых DTO и Value Objects с обязательными полями, где важна минимальная длина кода и максимальная производительность.
А непозиционный record дает разработчику больше гибкости: позволяет использовать инициализатор объекта для опциональных полей, добавлять приватные члены, сложную логику в конструкторах и явно контролировать наследование, но требует ручной реализации деконструкции и больше кода.
Также, можно использовать смешанный подход, как в примере ниже:
// Комбинируем позиционный синтаксис с дополнительными свойствами public record Employee( string Name, string Position ) { public DateTime HireDate { get; init; } public decimal Salary { get; init; } public int YearsOfService() => DateTime.Now.Year - HireDate.Year; }
В свою очередь смешанный подход комбинирует лучшее из обоих: позиционный синтаксис для обязательных полей и тело record для опциональных свойств и методов.
Таким образом, выбор зависит от сценария: позиционные records — для 80% случаев работы с данными, непозиционные — когда нужна гибкость инициализации или сложная логика.
Полный рабочий пример: Система управления заказами
В завершении статьи давайте соберем все вместе в реальном сценарии. Напишем приложение для системы управления заказами.
using System; using System.Collections.Generic; using System.Linq; // Базовый record для человека public record Person(string Name, string Email); // Наследование record class public record Customer(string Name, string Email, string CustomerLevel) : Person(Name, Email); // Record struct для адреса (маленький, часто создается) public record struct Address(string City, string Street, string Building); // Record для товара public record Product(string Id, string Name, decimal Price); // Record для позиции заказа public record OrderItem(Product Product, int Quantity) { public decimal Total => Product.Price * Quantity; } // Record для заказа public record Order( int Id, Customer Customer, Address ShippingAddress, List<OrderItem> Items, DateTime CreatedAt ) { public decimal Total => Items.Sum(i => i.Total); public string Status { get; init; } = "New"; public override string ToString() => $"Order #{Id} for {Customer.Name} - Total: {Total:C} ({Status})"; } class Program { static void Main() { // Создаем продукты var laptop = new Product("L001", "Ноутбук", 75000m); var mouse = new Product("M001", "Мышь", 1500m); // Создаем клиента var customer = new Customer("Алексей Иванов", "alex@example.com", "Gold"); // Создаем адрес var address = new Address("Москва", "Тверская", "15"); // Создаем заказ var order = new Order( Id: 1001, Customer: customer, ShippingAddress: address, Items: new List<OrderItem> { new(laptop, 1), new(mouse, 2) }, CreatedAt: DateTime.Now ); Console.WriteLine("=== Исходный заказ ==="); Console.WriteLine(order); Console.WriteLine($"Количество позиций: {order.Items.Count}"); Console.WriteLine($"Сумма: {order.Total:C}"); // Иммутабельное изменение статуса var updatedOrder = order with { Status = "Processing" }; Console.WriteLine("\n=== Обновленный заказ ==="); Console.WriteLine(updatedOrder); // Проверка иммутабельности Console.WriteLine("\n=== Исходный заказ не изменился ==="); Console.WriteLine(order); // Сравнение заказов var duplicateOrder = order with { Id = 1002 }; Console.WriteLine($"\n=== Сравнение ==="); Console.WriteLine($"order == duplicateOrder: {order == duplicateOrder}"); // False (разные Id) Console.WriteLine($"order.GetHashCode(): {order.GetHashCode()}"); Console.WriteLine($"duplicateOrder.GetHashCode(): {duplicateOrder.GetHashCode()}"); // Использование в словаре var orderDictionary = new Dictionary<Order, string>(); orderDictionary[order] = "Активный заказ"; // Поиск по значению var searchOrder = order with { }; // Создаем копию Console.WriteLine($"\nПоиск по копии: {orderDictionary[searchOrder]}"); // Работает! // Деконструкция заказа var (id, cust, shipAddr, items, date) = order; Console.WriteLine($"\nДеконструкция: Заказ #{id} для {cust.Name}"); // Работа с вложенными records и with var newAddress = address with { Building = "20", Street = "Петровка" }; var orderWithNewAddress = order with { ShippingAddress = newAddress }; Console.WriteLine($"\n=== Новый адрес доставки ==="); Console.WriteLine(orderWithNewAddress.ShippingAddress); } }
Запустите этот код и проанализируйте вывод. Вы увидите, как records делают код чистым, безопасным и лаконичным.
Заключение
В этой статье мы рассмотрели использование Records в C#. Важно понимать, что records это не просто «синтаксический сахар», а это фундаментальное изменение подхода к работе с данными в C#. С помощью records мы можем устранить тонны бойлерплейт‑кода, а также сделать код более читаемым и поддерживаемым. Помимо этого, данный инструмент позволяет предотвратить целый класс ошибок благодаря иммутабельности.
В итоге, если вы до сих пор пишете обычные классы для DTO и моделей данных — самое время перейти на records. Ваш код станет короче, чище и безопаснее.
Освоить C# с нуля проще, когда обучение выстроено последовательно, а темы не разваливаются на отдельные куски. Курс «C#-разработчик. Базовый уровень» помогает собрать прочную базу, разобраться в логике языка и перейти от знакомства с синтаксисом к нормальной практике разработки.льно можно опираться в работе.

Задача не просто выбрать курс, а войти в обучение в момент, когда это и полезно, и финансово разумно. 30–31 марта в честь дня рождения OTUS это можно сделать выгоднее: действует дополнительная скидка 10% по промокоду birthday на любые курсы, и она суммируется с другими скидками. Так что тем, кто давно планировал покупку, лучше не переносить решение. ☛
[Забрать курс со скидкой]
Если пока не хватает уверенности, чтобы заходить в большое обучение, открытые уроки помогут спокойно проверить интерес к теме и посмотреть, как объясняются важные технические вещи:
13 апреля в 20:00 — «Как работают структуры данных C# под капотом».
☛ [На урок по структурам данных C#]
21 апреля в 20:00 — «Производительность кода на примере алгоритмов сортировки».
☛ [На урок по алгоритмам сортировки]
