Avatar ·

Когда в C# использовать struct вместо class? Разбираемся с типами-значениями

Всем привет! Я недавно начал изучать C# и постоянно путаюсь, когда нужно выбирать между struct и class. Вроде бы они похожи — и там, и там можно объявить поля, методы, свойства. Но я слышал, что struct — это тип-значение, а class — ссылочный тип, и это сильно влияет на производительность. У меня есть конкретная задача: я пишу небольшой 2D-движок для игр. У меня есть структура Point с координатами X и Y. Сейчас она объявлена как class: ```csharp public class Point { public int X { get; set; } public int Y { get; set; } public Point(int x, int y) { X = x; Y = y; } } ``` Всё работает, но я создаю тысячи таких точек каждый кадр, и приложение начинает тормозить. Друг посоветовал заменить class на struct, чтобы ускорить работу. Но я боюсь, что это сломает код, потому что struct ведёт себя по-другому при копировании. Когда вообще стоит использовать struct? Есть ли какие-то чёткие правила? И что будет, если я просто заменю слово class на struct в моём коде — могут ли возникнуть проблемы? Как проверить, что моя структура не нарушает никаких правил? Заранее спасибо за помощь! Примеры кода приветствуются. <<>>

Основные критерии выбора struct вместо class

Вы правы — замена class на struct в вашем случае с Point — это правильное решение. Давайте разберёмся, почему и когда это уместно.

Когда struct — хороший выбор

По рекомендациям Microsoft (и здравому смыслу), используйте struct, если выполняется большинство из следующих условий:

  • Тип представляет собой простое значение (например, координаты, RGB-цвет, размер).
  • Экземпляры типа маленькие (обычно до 16-24 байт).
  • Тип неизменяемый (immutable) — после создания его состояние не меняется.
  • Вы создаёте много короткоживущих экземпляров (как в вашем игровом движке).
  • Тип не требует наследования (struct не может наследовать от другого struct или class).

Что изменится, если заменить class на struct

Давайте перепишем ваш Point как struct правильно:

public readonly struct Point
{
    public int X { get; } // Только для чтения
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // Для удобства можно добавить методы
    public double DistanceTo(Point other)
    {
        int dx = X - other.X;
        int dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

Ключевые отличия в поведении

1. Копирование по значению. При присваивании struct копируется целиком:

Point a = new Point(10, 20);
Point b = a; // b — полная копия, независимый объект
a = new Point(30, 40); // b остаётся (10,20)

2. Параметры методов. По умолчанию struct передаётся по значению (копируется). Для больших структур это может быть медленнее, чем class. Используйте ref или in для передачи по ссылке:

public void Move(ref Point p, int dx, int dy)
{
    p = new Point(p.X + dx, p.Y + dy);
}

3. Null-безопасность. Struct не может быть null. Если вам нужно nullable-значение, используйте Point?.

Производительность: почему struct ускорит ваш движок

  • Выделение на стеке — struct создаётся на стеке (или встраивается в массив/объект), что быстрее, чем выделение в куче для class.
  • Меньше нагрузки на GC — тысячи struct не создают мусора в куче, сборщик мусора не тратит время на их очистку.
  • Лучшая локальность данных — массив struct хранит данные последовательно в памяти, что ускоряет кэширование процессора.

Когда struct — плохой выбор

  • Если структура большая (> 24 байт) — копирование становится дорогим.
  • Если нужна изменяемость — мутабельные struct приводят к трудноуловимым багам.
  • Если тип должен участвовать в иерархии наследования.
  • Если вы часто используете boxing (например, передаёте struct в методы, ожидающие object).

Практический совет для вашего движка

Сделайте Point readonly struct, как показано выше. Для хранения тысяч точек используйте массив:

Point[] points = new Point[10000];
// Инициализация
for (int i = 0; i < points.Length; i++)
{
    points[i] = new Point(i, i * 2);
}

Это будет работать в разы быстрее, чем массив классов, и не создаст мусора для GC.

Итоговое правило

Используйте struct для маленьких, неизменяемых типов-значений, которые логически являются единым целым (координаты, вектор, цвет). Для всего остального — class.

<<>>

Альтернативный взгляд: когда struct может навредить

Согласен с основным ответом, но хочу предостеречь от слепого использования struct ради производительности. Я видел проекты, где разработчики заменили все class на struct и получили падение производительности.

Основная проблема — копирование при передаче в методы. Если ваша struct больше 16 байт и вы передаёте её в цепочку методов без ref, каждый вызов создаёт копию. В итоге вы тратите больше времени на копирование, чем экономите на GC.

Ещё один подводный камень — изменяемые struct. Посмотрите на этот код:

public struct MutablePoint
{
    public int X;
    public int Y;
}

var points = new MutablePoint[100];
points[0].X = 5; // Работает
var p = points[0];
p.X = 10; // Не меняет points[0]! Сюрприз!

Мой совет: если сомневаетесь — начинайте с class. Профилируйте код. Если видите, что GC — узкое место, и ваши объекты маленькие и неизменяемые — только тогда переходите на struct. Для 99% бизнес-приложений разница незаметна, а class проще и безопаснее.

Для вашего движка: попробуйте сделать Point struct, но обязательно замерьте производительность до и после. Используйте BenchmarkDotNet для точных замеров.