JavaRush /Курсы /C# SELF /Знакомство с record и...

Знакомство с record и позиционный синтаксис

C# SELF
19 уровень , 1 лекция
Открыта

1. Проблема передачи данных

Допустим, у нас есть приложение для школы, и нам нужно передавать информацию о студенте между разными модулями: имя, год рождения, класс. Как мы обычно поступаем?

Мы создаём класс типа:


public class Student
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}
Класс для хранения данных о студенте (старый подход)

Выглядит привычно. Но у такого подхода есть несколько проблем:

  • Чтобы сравнить двух студентов на равенство, по умолчанию будет сравниваться только ссылка на объект. То есть два студента с одинаковыми полями, но разными объектами — не равны!
  • Класс можно изменить после создания, что иногда ведёт к ошибкам (особенно если объект уже где-то используется);
  • Много «шаблонного» кода: конструкторы, методы сравнения, копирования (клонов), ToString.

И как вы уже, наверное, догадались: C# может избавить нас от этих забот! Встречайте — record.

2. Что такое record?

record — это особый тип в C#, который проектировался специально для хранения данных. У записи (record) есть две главные фичи:

  1. Неизменяемость (immutability): объекты типа record по умолчанию неизменяемы, то есть значения их свойств устанавливаются один раз при создании и больше не меняются (точнее, set-сеттеры приватные). Хотя можно объявлять и изменяемые записи, по умолчанию — неизменяемость.
  2. Сравнение по значению: если два record-объекта имеют одинаковые значения во всех своих полях, они считаются равными (== и .Equals() работают по-другому!).

На самом деле, record — это идеальная обёртка для передачи данных между слоями приложения (например, из БД в контроллер, из контроллера в представление и т.д.).

3. Синтаксис record

Самый простой способ — позиционный синтаксис

Когда нам нужно просто передать набор значений, мы объявляем тип с помощью одной короткой строчки:


public record Student(string Name, int YearOfBirth, string Class);
Позиционный синтаксис записи (record)

Что происходит под капотом? Компилятор сам сгенерирует для нас:

  • Автоматические свойства только для чтения (с приватным сеттером);
  • Конструктор, принимающий все параметры;
  • Методы сравнения и копирования;
  • Классный ToString, который нормально форматирует вывод!

Использование позиционного record

Давайте попробуем воспользоваться этим новым типом в нашем школьном приложении:

var student1 = new Student("Иван", 2008, "8А");
var student2 = new Student("Мария", 2008, "8Б");

Доступ к свойствам осуществляется как обычно (только их менять нельзя):

Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");

Попытка изменить свойство после создания

student1.Name = "Петр"; // Ошибка! Свойство только для чтения.

Если вы такую строку раскомментируете — компилятор тут же начнёт возмущаться: невозможно установить значение для только для чтения.

Вот так выглядит автоматически сгенерированный ToString

Console.WriteLine(student1); // Выведет: Student { Name = Иван, YearOfBirth = 2008, Class = 8А }

Всё красиво и понятно даже без ручного форматирования!

4. Сравнение record-объектов

Напомню: если создать два разных объекта классического класса с одинаковыми данными, они всё равно будут не равны друг другу:

var a = new Student("Иван", 2008, "8А");
var b = new Student("Иван", 2008, "8А");

Console.WriteLine(a == b); // Для класса: false

Если же Student — это record, то равенство работает так, как вы бы и хотели:


public record Student(string Name, int YearOfBirth, string Class);

var a = new Student("Иван", 2008, "8А");
var b = new Student("Иван", 2008, "8А");

Console.WriteLine(a == b); // Для record: true!
Сравнение record-объектов по значению

То есть две записи с одинаковыми полями приравниваются друг к другу даже если это два разных объекта в памяти.

5. Как это выглядит внутри

Студенты часто удивляются, сколько всего компилятор делает за нас в случае с record. Давайте сравним для наглядности, какой бы код пришлось писать вручную для класса и что делает record.

Старый добрый класс написанный руками

public class Student
{
    public string Name { get; }
    public int YearOfBirth { get; }
    public string Class { get; }

    public Student(string name, int yearOfBirth, string @class)
    {
        Name = name;
        YearOfBirth = yearOfBirth;
        Class = @class; //ссылка на свой собственный класс
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Student other) return false;
        return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, YearOfBirth, Class);
    }

    public override string ToString()
    {
        return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
    }
}

Неудивительно, что программисты становятся параноиками — мы ведь по 10 раз пишем одно и то же!

Record — одна строка


public record Student(string Name, int YearOfBirth, string Class);
Record — всё то же самое, но одной строкой!

6. Record и неизменяемость: что можно, а что нельзя

У record-ов по умолчанию свойства только для чтения, и это здорово предотвращает кучу багов. Но если очень хочется (например, вы пишете для очень старого API), можно объявить и изменяемые записи:

public record MutableStudent
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}

Теперь эти поля можно менять, но вы теряете часть преимуществ (безопасность, например).

7. Деструктуризация record

Поскольку позиционный синтаксис очень похож на кортежи, можно легко деструктурировать запись:


var student = new Student("Иван", 2008, "8А");

var (name, year, className) = student;

Console.WriteLine($"{name} - {year}, {className}"); // Иван - 2008, 8А
Деструктуризация позиционного record

Компилятор генерирует метод Deconstruct для каждого позиционного record, и это облегчает работу с LINQ, switch-паттернами и вообще делает жизнь приятнее.

8. Record — это класс или struct?

По умолчанию record — это ссылочный тип, как класс. То есть все особенности поведения ссылочных типов (хранение в куче, копирование ссылки и т.д.) работают обычным образом.

Если хочется значимый тип (value type), то в C# и для этого есть свой оператор — всегда можно написать:


public record struct Point(int X, int Y);
Record struct — значимый тип записи

Но для передачи данных почти всегда используется классическая форма records, то есть ссылочный тип. Подробнее про record struct — в ближайших лекциях :P

Сравнение class, struct, record

Тип Иммутабельность по умолчанию Сравнение по значению Легкая деструктуризация Авто ToString
class Нет Нет (по ссылке) Нет Нет
struct Нет Да Нет Нет
record Да Да Да Да

9. Особенности и типичные ошибки

Многие новички путаются в нюансах использования records. Например, иногда ожидают, что изменение поля у одного объекта record изменит другой (как у классов с копированием ссылки). Нет! Кроме того, когда пишете with, помните, что всегда создаётся копия, а не изменяется исходник. Записи идеально подходят для логики, где ваше приложение заботится о чистоте данных и предсказуемости.

Да, если вы объявили поля с init вместо set, их тоже можно задавать только при создании или с помощью with, но не позже.

2
Задача
C# SELF, 19 уровень, 1 лекция
Недоступна
Сравнение record по значению
Сравнение record по значению
2
Задача
C# SELF, 19 уровень, 1 лекция
Недоступна
Деструктуризация record
Деструктуризация record
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 35 Student
24 ноября 2025
Пишется про With без обьяснения, видимо будет дальше?