Что такое record в C#?

«Что такое record в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Record — это новый ссылочный тип (record class, по умолчанию) или тип-значение (record struct), представленный в C# 9.0 и улучшенный в последующих версиях. Его основное предназначение — моделирование неизменяемых (immutable) данных с семантикой равенства по значению (value-based equality).

Ключевые характеристики и "магия" компилятора:

  1. Неизменяемость по умолчанию (Positional Records): Свойства, объявленные в первичном конструкторе, автоматически становятся init-only (инициализируются при создании и не могут быть изменены).
  2. Равенство по значению: Для records автоматически генерируются методы Equals(object?), GetHashCode() и операторы ==/!=, которые сравнивают значения всех свойств, а не ссылки на объект.
  3. Метод ToString(): Автоматически генерирует удобное строковое представление, включающее имя типа и значения свойств.
  4. Поддержка деконструкции (Deconstruction): Для positional records генерируется метод Deconstruct, позволяющий легко разложить объект на переменные.
  5. Выражение with (Non-destructive mutation): Позволяет создать копию record с изменением одного или нескольких свойств, не изменяя оригинал.

Практические примеры:

// 1. Positional record (самый частый вариант)
public record Person(string FirstName, string LastName, int Age);

var person1 = new Person("John", "Doe", 30);
var person2 = new Person("John", "Doe", 30);
var person3 = person1 with { Age = 31 }; // Создание модифицированной копии

Console.WriteLine(person1 == person2); // True (равенство по значению)
Console.WriteLine(person1); // Person { FirstName = John, LastName = Doe, Age = 30 }
Console.WriteLine(person3); // Person { FirstName = John, LastName = Doe, Age = 31 }

var (fName, lName, age) = person1; // Деконструкция

// 2. Record с явно объявленными свойствами (если нужна валидация или логика)
public record Product
{
    public Guid Id { get; init; } // init-only для неизменяемости
    public string Name { get; init; }
    public decimal Price { get; init; }

    public Product(string name, decimal price)
    {
        if (price < 0) throw new ArgumentException("Price must be non-negative.");
        Id = Guid.NewGuid();
        Name = name;
        Price = price;
    }
}

// 3. Record struct (C# 10+)
public readonly record struct Point(int X, int Y); // readonly для полной неизменяемости

Идеальные сценарии использования:

  • DTO (Data Transfer Objects) для API-запросов и ответов.
  • Модели команд и событий в архитектурах CQRS/Event Sourcing.
  • Ключи в словарях (благодаря корректно реализованным Equals и GetHashCode).
  • Любые объекты-значения, где важна неизменяемость и сравнение по содержанию.