Menu

Структуры в C#: типы значений, struct или class и когда их использовать

Как работают структуры в C#: семантика значений и копирование при присваивании, ошибка изменения элемента List<T>, правила конструкторов, равенство, упаковка, readonly struct и record struct, а также сравнение struct и class с рекомендациями, когда структура это правильный выбор.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Структура это тип значения, который вы определяете сами. Она выглядит как класс, с полями, свойствами, методами и конструкторами, но переменная типа структуры содержит данные напрямую, а не ссылку на объект. Одно это отличие меняет поведение присваивания, вызовов методов, равенства и null.

Семантика значений: присваивание копирует

Встроенные числовые типы, bool, char, DateTime, TimeSpan и Guid это структуры. Ваши собственные работают так же:

Вывод:

struct: s1.X = 1, s2.X = 50
class:  c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150

Со структурой s2 это независимая копия, и Move тоже работает со своей копией. Чтобы метод мог изменить структуру вызывающего кода, передайте её по ссылке через ref (см. ref и out). Чтобы не копировать большую структуру, но и не разрешать изменения, C# 7.2 добавил параметры in.

Другие следствия того, что это тип значения:

  • Переменная-структура никогда не может быть null. PointS p = null; не компилируется. Используйте PointS? (тип значения, допускающий null), когда «нет значения» имеет смысл.
  • Неинициализированная структура состоит из нулей: числовые поля 0, поля bool равны false, ссылочные поля null.
  • Структура не может наследоваться от другой структуры или класса, и от структуры ничего не может наследоваться. Реализовать интерфейсы она может.
  • Локальные переменные-структуры обычно находятся в стеке или внутри содержащего их объекта, поэтому создание структуры не выделяет память в куче.

Ловушка списка структур: CS1612

Поскольку чтение структуры даёт копию, эта невинная на вид строка не компилируется:

var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable

Индексатор List<T> это метод, который возвращает копию элемента. Изменение X у этой временной копии ничего бы не изменило, поэтому компилятор вас останавливает. Та же ошибка возникает, когда свойство возвращает структуру: order.Location.X = 10. Скопируйте, измените, запишите обратно:

Вывод:

35
7

Массивы это исключение: array[0] это сам элемент, а не копия. Эта повторяющаяся боль и есть причина стандартного совета делать структуры неизменяемыми: если структуру нельзя изменить, изменение копии ничего не теряет.

Конструкторы и значения по умолчанию

В C# с 7 по 9 правила для конструкторов структур строгие:

  • Нельзя объявить конструктор без параметров. new PointS() существует всегда и обнуляет все поля.
  • Объявленный вами конструктор должен присвоить каждое поле (и автосвойство) до возврата.
  • Инициализаторы полей (public int X = 1;) для полей экземпляра не допускаются.

Вывод:

19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)

Money также показывает, почему важно значение по умолчанию: у обнулённого Money валюта равна null, и ваш код должен это обрабатывать, потому что массивы, default(T) и неинициализированные поля создают именно такое значение.

Более новые версии смягчили эти правила:

// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
    public int Retries = 3;
    public Settings() { }
}

// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.

Конструктор без параметров из C# 10 выполняется для new Settings(), но не для default(Settings) или элементов массива, которые по-прежнему состоят из нулей. Это расхождение удивляет людей, поэтому используйте его осторожно.

readonly struct (C# 7.2)

Если пометить саму структуру readonly, компилятор будет следить за неизменяемостью: каждое поле должно быть readonly, а каждое автосвойство только для чтения.

public readonly struct Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius) { Celsius = celsius; }

    public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta);   // returns a new value
}

Помимо документирования намерения, это помогает производительности: когда структура без readonly хранится в поле readonly или передаётся как параметр in, компилятор копирует её перед каждым вызовом метода (он не может знать, что метод её не изменит). readonly struct таких защитных копий не требует. В C# 7.0 всё равно можно сделать каждое поле readonly, как Money делает со свойствами только для чтения: это даёт неизменяемость, но не экономию копий, потому что компилятор доверяет только структуре, объявленной readonly.

Равенство

Equals у структуры по умолчанию сравнивает поле за полем, и это нужное поведение значения. Но реализация по умолчанию (ValueType.Equals) может использовать рефлексию и работает медленно, а оператор == вообще не определён: a == b для вашей структуры это ошибка CS0019. Реализуйте оба, если структуру будут сравнивать:

Вывод:

True
True
True

Реализация IEquatable<T> важна для коллекций: HashSet<T>, Dictionary<TKey, TValue> и List<T>.Contains вызывают Equals(GridCell) напрямую, а не упаковывают каждое значение ради вызова Equals(object). Структуры-записи (ниже) генерируют всё это за вас.

Упаковка

Преобразование структуры в object или в тип интерфейса упаковывает её: среда выполнения копирует значение в новый объект в куче. После этого упаковка и оригинал независимы:

Вывод:

2
0

Упаковка каждый раз стоит выделения памяти, поэтому старые необобщённые коллекции вроде ArrayList были медленными с типами значений, и поэтому их заменили обобщения. Это также означает, что изменяемая структура, к которой обращаются через интерфейс, меняется в своей упаковке, а не в оригинале, и это ещё одна причина делать структуры неизменяемыми.

struct или class

structclass
Видтип значенияссылочный тип
Присваивание и параметрыкопируют данныекопируют ссылку
Может быть nullнет (T? может)да
Значение по умолчаниювсе поля обнуленыnull
Наследованиенет; может реализовать интерфейсыодин базовый класс, интерфейсы
==не определён, если не перегрузитьравенство ссылок, если не перегружен
Equals по умолчаниюсравнивает полясравнивает ссылки
Размещениена месте (стек или содержащий объект)куча, сборка мусора
Подходит длянебольших неизменяемых значенийсущностей, большого или общего состояния

Когда использовать структуру

Выбирайте структуру, когда выполняется всё сразу: тип это одно логическое значение (координата, денежная сумма, цвет, диапазон дат), он небольшой (рекомендация Microsoft около 16 байт, примерно четыре int), неизменяемый, и наследование не нужно. Структуры окупаются, когда их создаётся очень много, например миллионы точек в массиве, потому что они избегают выделения в куче и записи в сборщике мусора на каждый элемент.

Выбирайте класс для всего, у чего есть идентичность (клиент, заказ), для всего большого, для всего, что изменяется из нескольких мест, и всякий раз, когда не уверены. Большая изменяемая структура даёт вам затраты на копирование плюс путаницу с изменением копий.

record struct (C# 10)

C# 10 добавил record struct, который одной строкой генерирует для структуры равенство по значению, ==, ToString и поддержку with:

public readonly record struct Coordinate(double Lat, double Lng);

var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same);            // True
Console.WriteLine(home);                    // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };

У обычного record struct позиционные свойства изменяемые; readonly record struct делает их доступными только для инициализации, и обычно это то, что нужно. Классовая версия и члены, которые пишет компилятор, описаны на странице о записях.

Частые ошибки

  • Изменение структуры через копию. list[0].X = 1 (CS1612), переменная foreach (CS1654), результат геттера свойства. Записывайте изменённую копию обратно или делайте структуру неизменяемой.
  • Большие структуры. Каждое присваивание и вызов копируют её целиком. После нескольких полей класс обычно быстрее.
  • Забытое обнулённое значение по умолчанию. Массивы и default(T) создают значения структуры без запуска вашего конструктора, поэтому проверяемые там поля всё равно могут оказаться нулём или null.
  • Сравнение через == без его определения. CS0019. Реализуйте IEquatable<T> и операторы или используйте record struct.
  • Изменяемые структуры за интерфейсами. Из-за упаковки изменение происходит с копией, на которую вы не смотрите.

Часто задаваемые вопросы

Что такое структура в C#?

Структура это пользовательский тип значения: struct Point { public int X; public int Y; }. Переменная типа структуры хранит сами данные, а не ссылку на объект, поэтому присваивание и передача в метод копируют всё значение. int, double, DateTime и Guid это структуры.

Чем структура отличается от класса в C#?

Класс это ссылочный тип: переменные разделяют один объект, и null допустим. Структура это тип значения: у каждой переменной своя копия, она не может быть null (если не использовать Point?), не может наследоваться и от неё нельзя наследоваться, а её значение по умолчанию это все поля, обнулённые. Структуры подходят для небольших неизменяемых значений; классы для сущностей с идентичностью и поведением.

Когда использовать структуру вместо класса в C#?

Когда тип представляет одно небольшое значение (координату, денежную сумму, диапазон дат), желательно неизменяем, сравнивается по содержимому и создаётся в больших количествах, где важно избегать выделений в куче. Рекомендации Microsoft добавляют размер примерно 16 байт или меньше. Если у типа есть идентичность, много полей или его изменяют через ссылки, используйте класс.

Почему возникает ошибка «Cannot modify the return value because it is not a variable»?

Это ошибка CS1612, обычно из-за list[0].X = 5 для List<Point> из структур. Индексатор возвращает копию структуры, поэтому изменение копии было бы потеряно, и компилятор отказывается. Скопируйте элемент в переменную, измените его и присвойте обратно: var p = list[0]; p.X = 5; list[0] = p;. У массивов этой проблемы нет, потому что array[0] ссылается на сам элемент.

Что такое readonly struct в C#?

readonly struct (C# 7.2) объявляет, что ни один член структуры не изменяет её состояние: все поля должны быть readonly, а автосвойства только для чтения. Компилятор это проверяет, и это позволяет ему избегать защитных копий, когда структура передаётся через in или хранится в поле readonly. Большинство структур стоит делать readonly.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ