Привет! Меня зовут Анна Дресвянникова. Я бэкенд-разработчица в компании ЦВТ. В .Net, помимо типов class и struct, с недавних пор есть еще и типы record и record struct. Появление двух новых типов могло внести запутанность по поводу того, в чем их сходства и различия, и в каких случаях стоит их применять. В этой статье мы разберем, в каких ситуациях использовать class, record, struct, и какие преимущества даёт тот или иной тип в различных ситуациях. В конце мы приведем шпаргалку, которая поможет быстро определиться, какой из видов типов лучше подходит в том или ином случае. В 9-й версии C# появился тип record (он же record class). Он позволяет создать нечто среднее между class и struct. А в 10-ой версии C# появился  record struct. Как можно догадаться, это что-то среднее между record и struct. Подробное описание этих типов есть в документации, а я напомню лишь основные моменты. Характеристика сlass record (он же record class) record struct struct Ссылочный/значимый ссылочный ссылочный значимый значимый Есть ли позиционная запись нет да да нет Неизменяемость (иммутабельность) по умолчанию   нет с позиционной записью — да  нет нет Наследование   да только от record нет нет Реализация интерфейсов    да да да да Автоматически генерируются операторы сравнения    сравнение по ссылке сравнение по значению сравнение по значению нет Создание через инициализатор объектов     да  с позиционной записью — нет    да да Автоматически генерируется конструктор без параметров да с позиционной записью — нет да да Основные особенности у record вот какие: позиционная запись; неизменяемость при использовании позиционной записи; создание нового экземпляра на основе уже существующего экземпляра (ключевое слово with); реализация ToString() по умолчанию возвращает значения публичных полей/свойств. Рассмотрим их по очереди. Основное преимущество позиционной записи — краткость: В большинстве случаев это сильно упрощает описание типа, позволяя обходиться одной строкой вместо десятка. Эта особенность record неразрывно связана со следующей. Мы можем также создать неизменяемый тип и без позиционной записи, с помощью init-свойств и readonly-полей. Но при добавлении новых свойств/полей очень просто забыть о необходимости явно пометить их как init или readonly. С позиционной записью в record такая ошибка исключается, так как при добавлении нового поля мы только добавляем в конструктор его тип и имя. Их неизменяемость при этом подразумевается по умолчанию. Замечание У record, описанного с помощью позиционной записи, нельзя переприсвоить значения свойствам, но их можно изменить через специальные методы. Ключевое слово with позволяет создать новый экземпляр на основе уже имеющегося с возможным изменением некоторых свойств/полей. Данная конструкция позволяет коротко записать создание нового неизменяемого экземпляра record, которые часть значений копирует из другого экземпляра record. Надо понимать, что этот способ является синтаксическим сахаром, и компилятор создаёт за вас много кода для реализации такого поведения, в том числе, специальный метод клонирования у базового типа, а также конструктор у копии. Замечание Копия создаётся неполная: если объект содержит ссылочное поле/свойство, то при копировании объекта не создается копия этого поля/свойства, а копируется только ссылка, ведущая на то же самое значение. Начиная с версии C# 10 для struct также можно использовать ключевое слово with. При попытке вызвать метод ToString() у типа record вы получите нечто вроде такого: В данном случае Person — это имя типа, а FirstName, LastName и ChildNames — его свойства со значениями. record struct имеет те же особенности, что и record, кроме неизменяемости при позиционной записи. По сути, record хорошо подходит в ситуации, когда нужно описать тип, обладающий следующими характеристиками: требуется неизменяемость объекта; объект будет передаваться между методами (чтобы передавалась ссылка на объект). Рассмотрим такие случаи детальнее В приложениях со сложной структурой нам часто требуется передавать данные между разными слоями или модулями приложения. Использовать для этого единственную модель — не очень удачное решение, так как, как правило, каждому отдельному слою нужен не весь объект, а только какая-то его часть. Используя общую модель, мы тратим ресурсы на передачу лишних данных и, возможно, передаем данные, которые передавать вообще не стоило бы. Пример: неавторизованный пользователь имеет меньше прав, чем авторизованный, поэтому мы должны передать ему меньше данных, чем авторизованному. То есть в данном случае нужно использовать 2 разных модели DTO. В таких случаях обычно применяют паттерн DTO. DTO (Data Transfer Object) — это объект, который содержит только данные (и никакой бизнес-логики) и предназначен для передачи данных от одного слоя приложения к другому. DTO как раз обладает описанными выше характеристиками (ну или должен обладать при хорошей архитектуре проекта): он нужен исключительно для передачи данных, он нужен для отправки только в 1 сторону, нет нужды в его изменении после получения. То есть, если вам нужно описать DTO, то ваш выбор — record с позиционной записью. В данном случае record не подходит. Давайте разберем, почему. Entity — это объект предметной области, который можно однозначно идентифицировать по уникальному идентификатору. Сущность должна обладать следующими характеристиками: иметь конструктор без параметров либо параметризованный конструктор (некоторые ORM могут работать только с конструктором без параметров, некоторые — с обоими случаями); должна быть изменяемой; объекты должны сравниваться по ссылке: сущности совпадают, если совпадают именно ссылки на их значения, а не сами значения свойств/полей. В данном случае мы не сможем использовать record с позиционной записью, так как он по умолчанию имеет только параметризованный конструктор. Если наша ORM требует конструктор без параметров, то нам придется создать цепочку из двух конструкторов: Или придётся описать record в традиционном стиле, тогда конструктор без параметров будет сгенерирован компилятором. Если мы используем Entity Framework Core, помните: если в нашей модели есть навигационные свойства, то EF Core не сможет их привязать, если эти свойства не описаны традиционно. Например, мы хотим использовать позиционную запись для Food: Здесь Category — навигационное свойство. Используя такую запись, при работе с БД мы получим ошибку: Есть два пути решения проблемы: описать record в традиционном стиле: явно описать только свойство Category  В первом случае мы не получаем преимущества краткой записи record. Во втором случае не очень удобно, когда часть свойств указана явно, а часть — только в конструкторе. Суммируя, приведу полный список причин, по которым record не очень подходит для описания entity: тип record с позиционной записью — неизменяемый, поэтому преимущество краткой записи мы не получим и придется использовать традиционную запись Entity Framework Core использует ссылочное равенство для сравнения двух сущностей; вряд ли нам понадобится использовать with с сущностями ToString() у record можно было бы использовать для записи в лог конкретных значений полей в сущности, с которой работаем. Но если у сущности есть ссылочные поля (а скорее всего они будут), то мы получим результат исполнения их метода ToString(), который при отсутствии переопределения вернет имя типа, а не значение свойства. То есть если мы хотим логировать значение объекта, то нам в любом случае придется переопределять метод ToString(). Таким образом, record не подходит для данного случая и не дает преимуществ перед обычными классами для описания entity. Если нужно создать большую коллекцию объектов, каким-то образом её обработать внутри метода, после чего она будет не нужна, то отличный вариант — использовать коллекцию структур. В данном случае все данные будут лежать в одной области памяти и при интенсивной работе с элементами такой коллекции есть высокая вероятность, что количество кэш-промахов будет меньше, чем при хранении в коллекции только ссылок на данные. Если же коллекцию нужно будет использовать не только внутри одного метода, либо вне метода будут использоваться элементы коллекции по отдельности, то лучше выбрать class или record. Выделение памяти для коллекции ссылочных типов будет заведомо более ресурсоёмким, а сборщику мусора понадобится больше времени на освобождение такой коллекции. При большом размере коллекции struct дает значительное преимущество по производительности, если структура содержит поля/свойства значимых типов (при выделении памяти для структуры с полями/свойствами ссылочных типов выигрыш по времени будет совсем незначительный). Сравнение с подробным объяснением можно посмотреть здесь. Если коллекция или отдельные элементы будут использоваться вне одного метода, то наиболее ожидаемое поведение элементов — поведение ссылочных объектов. Если элементы будут значимого типа, то это влечет потенциальные ошибки и просто неудобства. Если типы struct или record struct используются в generic-коллекции, то поля/свойства элементов таких коллекций являются неизменяемыми: Это вполне объяснимо, если вспомнить, что элементы структуры — это значимые типы, а доступ к элементам структуры через метод-индексатор должен возвращать ссылку на элемент. Поскольу невозможно вернуть ссылку на значимый тип, то в реальности создаётся копия элемента структуры, ссылка на которую и возвращается (бонусом идёт упаковка и распаковка). Чтобы обойти это ограничение, придется создать новый объект и записать его в elements[i]: При использовании массива такой проблемы нет, потому что доступ к элементам массива осуществляется напрямую. Тип struct подойдет, если нужно создать множество небольших объектов внутри метода, причем каждый объект будет жить очень недолго. Например, объект создается внутри цикла и умирает по завершении итерации. В этом случае стоит выделить память под объект на стеке и, как только он станет не нужен (то есть при выходе из области его видимости), память на стеке освободится автоматически. Даже если объект содержит значимые поля/свойства, то в итоге это всё равно будет быстрее, чем выделять память в куче и нагружать сборщик мусора для освобождения памяти. Бенчмарк наглядно демонстрирует справедливость этого утверждения: Method Mean Error StdDev ListOfClassesTest 199.6 ms 2.05 ms 1.82 ms ListOfStructsTest 167.5 ms 3.28 ms 4.15 ms При передаче struct в какой-то метод, передается не сам объект, а его копия (так происходит со всеми значимыми типами). Здесь возможно возникновение следующих двух проблем: неявное выделение памяти для копии; если изменится оригинальное значение (например, при использовании многопоточности), мы об этом не узнаем и будем работать с устаревшим значением. Если экземпляр значимого типа занимает не более 16 байт, то первая проблема не является проблемой вовсе. В этом случае даже оптимальнее передать значимый тип, чем ссылочный. Если же значимый тип слишком большой, то его экземпляр можно передать его по ссылке с помощью модификаторов ref или in. Вторую проблему можно решить двумя разными способами: либо использовать readonly-структуру (чтобы гарантировать отсутствие изменений), либо передавать структуру с модификаторами ref или in, чтобы значение не копировалось, а объект передавался по ссылке (тогда мы узнаем об изменении). Использование struct с in - не очень удачный вариант, поэтому лучше его не использовать, и для больших объектов применять class и record. И вот почему: при передаче с модификатором in изменяемой не-POCO (содержащей не только поля, но и методы) структуры при условии, что внутри метода идет обращение к её не-readonly членам, компилятор перед вызовом создает в стеке защитную копию структуры при каждом таком обращении. Защитная копия не будет создаваться только при обращении к readonly-членам структуры. Если с модификатором in передается readonly-структура или структура является POCO-объектом (то есть, содержит только поля), то защитная копия создаваться не будет. Но этот вариант ненадежен, так как POCO-объект легко по ошибке превратить в не-POCO, например, заменить поле на свойство. Если невозможно объявить структуру readonly, то поможет struct или record struct + in + обращение только к readonly-членам. Этот вариант достаточно хрупкий, так что его лучше не использовать. Вывод: если структура не больше 16 байт, то можно передавать ее в метод по значению. Если используется многопоточность и нужна гарантия, что другие потоки не изменят структуру, то объявить ее readonly struct. Если же объект больше 16 байт, то хоть выпутаться из описанных неприятностей можно, но приведенные решения достаточно хрупки и не очевидны. В этих случаях лучше использовать class и record, а не struct. Лучший выбор для неизменяемого объекта record с позиционной записью. Конечно, это могут быть class и record с readonly либо init-полями/свойствами, readonly struct, readonly record struct. Но все-таки record с позиционной записью дает защиту от потенциального нарушения неизменяемости при изменении/расширении списка полей.   Если тип используется преимущественно как член других типов, то для повышения производительности при выделении и освобождении памяти можно использовать struct, record struct. Нужно быть внимательными если поле/свойство объявлено с модификатором readonly и имеет при этом тип неиммутабельной структуры. В этом случае если в каком-то методе идет обращение к свойству или методу структуры, то при каждом таком обращении создается её защитная копия. Если же структура является POCO-объектом (содержит только поля), то защитная копия создаваться не будет, так как доступ к полям будет считаться безопасным. Возможные решения: объявить поле/свойство-структуру без readonly; объявить саму структуру readonly; сделать структуру POCO. 1 и 3 варианты достаточно хрупки. Второй вариант самый надежный, если разработчики, работающие с вашим кодом будут понимать, что структура является readonly не просто так, и изменять ее на изменяемую не нужно. Безопасный вариант — использовать class или record. С нюансами можно использовать struct, record struct. Да, структура может реализовывать интерфейсы. Но нужно быть осторожными, так как при обращении к экземпляру структуры через переменную, имеющую тип интерфейса, по умолчанию происходит упаковка/распаковка. В некоторых случаях мы можем защититься от этого. Например, при объявлении метода, в который мы хотим передать структуру под видом интерфейса, можно добавить ограничение: При объявлении поля, свойства или локальной переменной с типом интерфейса и сохранении в него структуры избежать упаковки не получится. Если объект представляет собой небольшое количество сгруппированных примитивных значимых типов и по смыслу он также является примитивом, то в этом случае логично использовать структуру. Пример: точка в n-мерном пространстве, вектор, особенная единица измерения. Хорошо известный пример из .NET — типы для даты и времени. Если нужно наследоваться от классов, то class. Если нужно наследоваться от record, то record. Вопрос применения наследования является неоднозначным. Наследование нужно точно, когда необходимо соблюсти принцип подстановки Лисков. Например, необходимо иметь возможность заменять передаваемый в метод класс на его потомков. В некоторых случаях его лучше заменить агрегацией. Но это уже тема другой статьи. По умолчанию struct, record struct, record подходят для сравнения по значению, а class — по ссылке. Но на деле можно сравнить class по значению, а record — по ссылке. Тип struct сравниваются только по значению, да и сравнение их по ссылке просто не имеет смысла. Тип class по умолчанию сравниваются по ссылке, но мы можем реализовать интерфейс IEquatable<T>, чтобы сравнить их по значению. Тип record по умолчанию сравниваются по значению, но мы можем использовать метод ReferenceEquals(obj1, obj2), чтобы сравнить их по ссылке. Тогда struct, record struct. Использовать структуру с полями/свойствами ссылочного типа конечно можно, но при копировании структуры будет скопировано не само значение ссылочного поля, а лишь ссылка на него. То есть, ссылочное поле внутри двух экземпляров структур будут ссылаться на одно и то же значение. Это не совсем очевидно для значимого типа и открывает возможность для ошибок. Откажитесь от struct. Упаковка и распаковка не только снижает производительность, но и может привести к ошибкам. Пример: упакуем один и тот же объект в две разные локальные переменные. Казалось бы, объект один и тот же, поэтому наши переменные должны ссылаться на один объект в куче. А на самом деле упаковка всегда создает новый объект, поэтому переменные ссылаются на два разных объекта. Еще одна ошибка возникает, когда мы что-то меняем в упакованном объекте и ожидаем, что изменится и оригинальный объект. Чтобы изменился оригинальный объект, нам нужно не забыть распаковать в него упакованный измененный объект. Эти вопросы касаются вашего личного удобства и принципиально не должны влиять на выбор. Тогда record и record struct. Тогда class, record в традиционной записи и record struct. Всё, кроме record с позиционной записью. Тогда class, record, record struct. Тогда record, struct и record struct (ключевое слово with).  ИТ-компания