Типы: value и reference, struct и class, CTS
↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая
Зачем это на собесе
вопрос №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: равенство по значению
}Вопросы с ответами
Чем value type отличается от reference type?
Переменная value type содержит сами данные, при присваивании и передаче они копируются. Reference type хранит ссылку на объект в куче: копируется ссылка, объект общий. Value types не бывают null (кроме Nullable), не наследуются.
Когда использовать struct вместо class?
Для небольших иммутабельных значений, которые логически представляют одно значение (деньги, координаты, интервал) и создаются в больших количествах. Это снижает нагрузку на GC.
string — value или reference type? Почему ведёт себя как значение?
Reference type, но иммутабельный, и оператор == сравнивает содержимое. Любое «изменение» создаёт новую строку.
Почему мутабельные struct считаются плохой практикой?
Из-за неявных копий (индексаторы, свойства, боксинг, readonly-поля) изменения часто применяются к копии и теряются. Это трудноуловимые баги.
Можно ли наследовать struct? Реализовать интерфейс?
Наследовать нельзя: struct неявно sealed и наследуется от ValueType. Реализовать интерфейсы можно, но приведение к интерфейсу вызывает boxing.
Связанные темы
- Предыдущая: Платформа .NET: SDK, runtime, версии, сборки, NuGet, solution и csproj · Следующая: Переменные, var, const и readonly, преобразования типов
- Стек, куча и boxing: Стек и куча, boxing и unboxing
- Память процесса: Память: стек, куча, виртуальная память
- Equals и GetHashCode: Equals, GetHashCode, сравнение объектов
- Records: Records, init, with, required, primary constructors
- Типы в JS (для сравнения): Типы данных и приведение типов