Что такое record в 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).
  • Любые объекты-значения, где важна неизменяемость и сравнение по содержанию.

Ответ 18+ 🔞

Давай разберемся с этими вашими record'ами, а то народ местами нихуя не понимает, что это за зверь такой и зачем он нужен.

Record — это, по сути, новый тип в C#, который появился в девятой версии и потом ещё допилили. Если коротко — это специальная форма класса или структуры, которая заточена под хранение данных, которые менять не планируется. Главная фишка — равенство по значению работает из коробки, без этой всей ебалы с переопределением Equals и GetHashCode.

Что там за магия происходит под капотом:

  1. Неизменяемость (в основном варианте): Свойства, которые ты объявляешь в конструкторе, становятся init-only. То есть задал раз при создании — и всё, потом нихуя не поменяешь. Удобно, чтобы случайно не накосячить.
  2. Равенство по значению, а не по ссылке: Это, блядь, самое важное. Два record'а считаются равными, если у них все свойства попарно равны. Компилятор сам генерит все нужные методы сравнения, не надо руками это городить. Просто красота.
  3. ToString() который не сосёт: Автоматом выводит что-то вменяемое типа Person { FirstName = John, LastName = Doe, Age = 30 }, а не название типа и хеш-код, от которого толку ноль.
  4. Деконструкция на раз-два: Можно сразу разобрать объект на составные части в переменные, без лишних телодвижений.
  5. Выражение with — копия с изменениями: Вот это вообще пиздец как удобно. Хочешь взять существующий объект, поменять в нём одно поле, а остальные оставить как есть, и чтобы оригинал не пострадал? Легко! Создаёшь копию через with. Никакого геморроя с клонированием.

Смотри, как это выглядит на практике:

// 1. Классический record (positional record)
public record Person(string FirstName, string LastName, int Age);

// Создаём
var ivan = new Person("Иван", "Иванов", 30);
var anotherIvan = new Person("Иван", "Иванов", 30);
// Меняем возраст, создавая нового Ивана
var ivanOlder = ivan with { Age = 31 };

// Магия равенства работает
Console.WriteLine(ivan == anotherIvan); // True! Потому что свойства одинаковые.
Console.WriteLine(ivan); // Выведет: Person { FirstName = Иван, LastName = Иванов, Age = 30 }
Console.WriteLine(ivanOlder); // Выведет: Person { FirstName = Иван, LastName = Иванов, Age = 31 }

// Деконструкция — разобрал как лего
var (name, surname, years) = ivan;

// 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("Цена не может быть отрицательной, ты чё, охренел?");
        Id = Guid.NewGuid();
        Name = name;
        Price = price;
    }
}

// 3. Record struct (появился в C# 10)
public readonly record struct Point(int X, int Y); // readonly чтобы наверняка ничего не сломал

Где это самое оно применять:

  • Объекты для передачи данных (DTO) в API — туда-сюда таскать, идеально.
  • Команды и события в CQRS или Event Sourcing — там неизменяемость прям просится.
  • Ключи в словарях — раз равенство по значению работает, то и в Dictionary всё будет как надо.
  • Вообще любые объекты, которые по смыслу являются "значениями" и которые нужно сравнивать по содержимому, а не по тому, где они в памяти лежат.