Типы: value и reference, struct и class, CTS

↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая

Обязательно~5 мин чтенияУровень: junior

Зачем это на собесе

вопрос №1 на C#-собесе: «чем value type отличается от reference type, struct от class». Отсюда растут boxing, копирование, Equals, производительность и баги с изменением копии.

Объяснение

Common Type System (CTS)

Все типы .NET наследуются от System.Object. Две категории:

  • Value types (struct, enum, встроенные числовые, bool, char, DateTime, Guid, decimal, Nullable<T>, кортежи ValueTuple) — наследуются от System.ValueType.
  • Reference types (class, interface, delegate, record (class), массивы, string, object).

Семантика копирования — главное отличие

struct PointS { public int X; }
class  PointC { public int X; }
 
var s1 = new PointS { X = 1 };
var s2 = s1;        // копия ВСЕХ данных
s2.X = 99;
Console.WriteLine(s1.X);  // 1
 
var c1 = new PointC { X = 1 };
var c2 = c1;        // копия ССЫЛКИ, объект один
c2.X = 99;
Console.WriteLine(c1.X);  // 99
  • Переменная value type содержит данные, reference type — ссылку на объект в куче.
  • Передача в метод по умолчанию — по значению: для struct копируются данные, для class — ссылка (метод может изменить объект, но не заменить ссылку вызывающего без ref).

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

  • Память: struct — inline (на стеке или внутри содержащего объекта), class — всегда куча + заголовок объекта (16 байт на x64) + ссылка.
  • null: struct не может быть null (только Nullable<T>), class может.
  • Наследование: struct нельзя наследовать (неявно sealed), но можно реализовывать интерфейсы.
  • Значение по умолчанию: struct — все поля нулевые (default), class — null.
  • Равенство: struct по умолчанию сравнивается по значениям полей (через рефлексию, медленно — переопределяйте Equals или используйте record struct), class — по ссылке.
  • Конструктор без параметров: у struct всегда есть (с C# 10 можно объявить свой).

Когда выбирать struct

Рекомендации Microsoft: небольшой (≲16 байт), логически одно значение (координата, деньги, диапазон дат), иммутабельный, редко боксится. Примеры в BCL: DateTime, TimeSpan, Guid, KeyValuePair.

public readonly record struct Money(decimal Amount, string Currency);   // иммутабельный, с равенством по значению

string — особый reference type

Ссылочный тип, но иммутабельный и сравнивается == по содержимому (перегружен оператор). Поэтому ведёт себя «как значение».

Специальные виды struct

  • readonly struct — все поля readonly, компилятор избегает защитных копий.
  • ref struct (Span<T>) — живёт только на стеке: нельзя боксить, хранить в полях класса, использовать в async (до C# 13 с ограничениями).

Нюансы и подводные камни

  • Мутабельная struct — источник багов:
var list = new List<PointS> { new() { X = 1 } };
// list[0].X = 5;           // ошибка компиляции: индексатор возвращает КОПИЮ
var p = list[0]; p.X = 5;   // меняется копия, в списке по-прежнему 1
  • Большие struct дорого копировать: передавайте через in или делайте class.
  • struct как ключ Dictionary без переопределённых Equals/GetHashCode — медленно (рефлексия и boxing). Используйте record struct или реализуйте IEquatable<T>.
  • Приведение struct к интерфейсу боксит её, и изменения через интерфейс идут в копию.
  • Большой массив struct — один объект в куче (хорошо для кэша CPU). Массив class — массив ссылок + N объектов.

Тестирование

[Fact]
public void Struct_is_copied_on_assignment()
{
    var a = new Money(10, "BYN");
    var b = a with { Amount = 20 };
    a.Amount.Should().Be(10);
    (a == new Money(10, "BYN")).Should().BeTrue();   // record struct: равенство по значению
}

Вопросы с ответами

Связанные темы