Когда использовать struct вместо class в C#: разбираемся с типами-значениями и ссылочными типами
Основные критерии выбора между struct и class
Начну с главного правила, которое рекомендуют в документации Microsoft и которое подтверждено практикой тысяч разработчиков: по умолчанию используйте class. Struct — это инструмент для оптимизации в специфических сценариях, а не замена class на каждый день.
Вот чеклист, когда struct оправдана:
- Размер данных ≤ 16-24 байта (обычно 2-3 поля простых типов)
- Короткое время жизни — объект создаётся и быстро уничтожается в рамках одного метода
- Неизменяемость — после создания поля не меняются (рекомендуется readonly struct)
- Частое создание в циклах — например, миллионы структур в секунду
- Не требуется наследование (struct не может наследовать от другого struct или class)
- Логически это единое значение — например, координаты, цвет, комплексное число
Разберём на вашем примере Point. Предположим, мы обрабатываем 10 миллионов точек:
// Тест производительности
var sw = System.Diagnostics.Stopwatch.StartNew();
var points = new PointClass[10_000_000];
for (int i = 0; i < points.Length; i++)
points[i] = new PointClass(i, i);
sw.Stop();
Console.WriteLine($"Class: {sw.ElapsedMilliseconds} ms");
sw.Restart();
var pointsStruct = new PointStruct[10_000_000];
for (int i = 0; i < pointsStruct.Length; i++)
pointsStruct[i] = new PointStruct(i, i);
sw.Stop();
Console.WriteLine($"Struct: {sw.ElapsedMilliseconds} ms");
Здесь struct будет значительно быстрее, потому что массив PointStruct[] хранит данные прямо в массиве (в куче, но непрерывно), а массив PointClass[] хранит ссылки на объекты, разбросанные по куче. Struct даёт лучшую локальность данных (cache locality).
Почему struct должен быть неизменяемым?
Посмотрите на этот код:
var p1 = new PointStruct(1, 2);
var p2 = p1; // Копирование!
p2.X = 10;
Console.WriteLine(p1.X); // 1 — p1 не изменился, всё ок
Но если вы передадите struct в метод:
void Mutate(PointStruct p) { p.X = 100; } // Работает с копией!
var p = new PointStruct(1, 2);
Mutate(p);
Console.WriteLine(p.X); // 1 — оригинал не изменился, это сбивает с толку
А если сделать struct изменяемым и использовать его в коллекции:
var list = new List<PointStruct> { new PointStruct(1, 2) };
list[0].X = 10; // Ошибка компиляции! list[0] — это копия, менять копию бессмысленно
Поэтому с C# 7.2 появился модификатор readonly:
public readonly struct PointStruct
{
public int X { get; }
public int Y { get; }
public PointStruct(int x, int y) { X = x; Y = y; }
}
Теперь компилятор гарантирует неизменяемость, и вы избегаете неожиданного копирования.
Когда НЕ стоит использовать struct
- Размер > 16-24 байт — копирование станет дороже, чем работа со ссылкой
- Частое использование в LINQ — методы вроде Where() или Select() создают много копий
- Асинхронные методы — async/await перемещает struct в кучу (boxing), теряя преимущества
- Наследование или полиморфизм — struct не поддерживает наследование
- Объекты с изменяемым состоянием — например, банковский счёт с балансом
Практический пример: когда struct — идеальный выбор
Допустим, вы пишете графический движок и работаете с цветами в формате RGBA:
public readonly struct Color
{
public byte R { get; }
public byte G { get; }
public byte B { get; }
public byte A { get; }
public Color(byte r, byte g, byte b, byte a = 255)
{
R = r; G = g; B = b; A = a;
}
// Методы, возвращающие новые экземпляры
public Color WithAlpha(byte alpha) => new Color(R, G, B, alpha);
public static Color Blend(Color a, Color b) { /* ... */ }
}
Цвет — это 4 байта (32 бита), идеально подходит под struct. Он неизменяем, часто создаётся в циклах обработки пикселей, и копирование 4 байт тривиально.
Итог: используйте struct только для маленьких, неизменяемых, логически единых значений, которые создаются в большом количестве. Во всех остальных случаях — class.
<<Дополнение: производительность и подводные камни
Согласен с основным ответом, но хочу добавить несколько важных нюансов, которые часто упускают новички.
1. Boxing — главный враг struct
Когда вы передаёте struct в метод, ожидающий object, или используете её в негенериковой коллекции (например, ArrayList), происходит упаковка (boxing) — struct копируется в кучу, что снижает производительность. Всегда используйте generic-коллекции (List<T>, Dictionary<K,V>), чтобы избежать этого.
2. Struct в качестве поля класса
Если у вас есть class, который содержит struct-поля, то при копировании класса копируется и struct (если это value-тип). Это может быть как преимуществом (глубокое копирование), так и неожиданностью. Например:
public class Container
{
public PointStruct Location; // поле-структура
}
var c1 = new Container { Location = new PointStruct(1, 2) };
var c2 = c1;
c2.Location.X = 10; // Ошибка! Location — это копия, но c1.Location не изменится
3. Используйте ref return для больших struct
Если вам нужно работать с большой struct без копирования, используйте ref и in параметры:
public void Process(in LargeStruct data) // in — передача по ссылке без копирования
{
// data — readonly, копирования не происходит
}
4. Когда struct всё же может быть больше 16 байт?
Если struct содержит только значимые типы и используется исключительно в локальных вычислениях (не передаётся между методами), то 32-48 байт тоже может быть оправдано. Но это уже уровень микрооптимизаций, который стоит применять только после профилирования.
Мой совет: начните с class, и только если профилировщик показал, что создание объектов занимает >5% времени, и struct действительно подходит по критериям — тогда заменяйте. Не оптимизируйте заранее!
Ответов пока нет.
Войдите чтобы оставить ответ