Структура это тип значения, который вы определяете сами. Она выглядит как класс, с полями, свойствами, методами и конструкторами, но переменная типа структуры содержит данные напрямую, а не ссылку на объект. Одно это отличие меняет поведение присваивания, вызовов методов, равенства и 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
| struct | class | |
|---|---|---|
| Вид | тип значения | ссылочный тип |
| Присваивание и параметры | копируют данные | копируют ссылку |
Может быть 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.