В чем разница между record и struct в C#?

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

Ответ

record и struct в C# предназначены для моделирования данных, но имеют принципиальные различия в поведении и предназначении.

Record (обычно record class)

  • Тип: Ссылочный тип (по умолчанию — record class). Хранится в управляемой куче (heap).
  • Неизменяемость: Создан для неизменяемых (immutable) данных. Свойства инициализируются при создании и не могут меняться (при использовании позиционного синтаксиса или init-сеттеров).
  • Сравнение по значению: Автоматически генерирует методы Equals, GetHashCode() и операторы ==/!=, которые сравнивают значения всех свойств, а не ссылки.
  • Другие фичи: Встроенная поддержка ToString() с выводом свойств, деконструкции (Deconstruct) и копирования с модификацией (with expressions).
public record Person(string Name, int Age); // Неизменяемый record

var person1 = new Person("Alice", 30);
var person2 = new Person("Alice", 30);

Console.WriteLine(person1 == person2); // True (сравнение по значению)
Console.WriteLine(person1); // Person { Name = Alice, Age = 30 }

var person3 = person1 with { Age = 31 }; // Неизменяемое копирование

Struct (значимый тип)

  • Тип: Значимый тип (value type). Хранится в стеке (stack) или внутри родительского объекта. Копируется по значению при присваивании.
  • Изменяемость: По умолчанию изменяем (mutable), если не объявлен как readonly struct.
  • Сравнение по значению: Не генерирует логику сравнения автоматически. По умолчанию используется ValueType.Equals, который работает через рефлексию и медленно. Для производительности нужно реализовывать IEquatable<T> и переопределять Equals/GetHashCode вручную.
  • Ограничения: Не может иметь конструктора без параметров (до C# 10), не поддерживает наследование.
public struct Point
{
    public int X;
    public int Y;
    // Нет автоматического сравнения по значению!
}

var point1 = new Point { X = 5, Y = 10 };
var point2 = point1; // Копирование значения
point2.X = 20; // Изменяется только копия

Ключевой выбор:

  • Используйте record для неизменяемых моделей данных, DTO, где критично сравнение по значению и нужна встроенная функциональность (ToString, with).
  • Используйте struct для небольших (менее 16 байт), часто создаваемых и копируемых объектов, где важна производительность и размещение в стеке (например, координаты, ключи словарей). Для неизменяемых структур объявляйте readonly struct.