Будет ли происходить боксинг (boxing) при сравнении двух структур (value types) в C#?

Ответ

Краткий ответ: Зависит от способа сравнения. При использовании переопределенного метода Equals() или оператора == для структуры боксинга не происходит. Боксинг может случиться при приведении структуры к типу интерфейса или при вызове унаследованного от object метода.

Детали и примеры:

  1. Сравнение БЕЗ боксинга (рекомендуемый способ):

    public readonly struct Point : IEquatable<Point>
    {
        public int X { get; }
        public int Y { get; }
        // Реализация IEquatable<T> избегает боксинга
        public bool Equals(Point other) => X == other.X && Y == other.Y;
        // Переопределение Equals(object) требует боксинга для 'obj', но это редко используемый путь
        public override bool Equals(object obj) => obj is Point other && Equals(other);
        // Перегрузка оператора ==
        public static bool operator ==(Point left, Point right) => left.Equals(right);
    }
    
    Point p1 = new Point(1, 2);
    Point p2 = new Point(1, 2);
    bool isEqual1 = p1.Equals(p2);        // НЕТ боксинга (вызов IEquatable<Point>.Equals)
    bool isEqual2 = p1 == p2;             // НЕТ боксинга (вызов перегруженного оператора)
  2. Сравнение, которое ВЫЗЫВАЕТ боксинг:

    Point p1 = new Point(1, 2);
    Point p2 = new Point(1, 2);
    
    // БОКСИНГ: Приведение к необобщенному интерфейсу
    IEquatable ie = p1; // Боксинг для p1!
    bool isEqual3 = p1.Equals((object)p2); // Боксинг для p2 (вызов Equals(object))
    
    // БОКСИНГ: Использование в коллекциях типа object (например, ArrayList)
    ArrayList list = new ArrayList();
    list.Add(p1); // Боксинг при добавлении!

Почему это важно? Боксинг создает новый объект в куче (heap), что приводит к дополнительным затратам на выделение памяти и последующую сборку мусора (GC). В высокопроизводительном коде или в циклах это может стать узким местом. Всегда реализуйте для структур интерфейс IEquatable<T> и используйте обобщенные коллекции (List<Point> вместо ArrayList).

Ответ 18+ 🔞

Давай разберёмся с этим вопросом про боксинг структур при сравнении, а то народ часто путается, как сука слепая в луже.

Вот смотри, если ты написал свою структуру и не поленился сделать всё по уму, то боксинг при сравнении не происходит. Но это только если ты не еблан и не используешь кривые пути.

Как правильно, чтобы не было боксинга:

Ты делаешь так — реализуешь IEquatable<T> для своей структуры. Это главная фишка. Вот смотри на пример, он как шпаргалка для неёбистов:

public readonly struct Point : IEquatable<Point>
{
    public int X { get; }
    public int Y { get; }

    // Вот это — наш козырь. Этот метод жрёт такую же структуру, без всякого боксинга.
    public bool Equals(Point other) => X == other.X && Y == other.Y;

    // А этот метод — legacy-говно, его вызывают старые коллекции. Тут бокс для `obj` будет, но если ты не дебил, ты редко сюда попадёшь.
    public override bool Equals(object obj) => obj is Point other && Equals(other);

    // И оператор сравнения перегрузим, почему бы и нет? Тоже без бокса.
    public static bool operator ==(Point left, Point right) => left.Equals(right);
}

// Используем
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

bool isEqual1 = p1.Equals(p2);        // Вызовется bool Equals(Point other) — НИКАКОГО БОКСА! Всё чисто.
bool isEqual2 = p1 == p2;             // Вызовется наш оператор — опять всё на стеке, красота.

Вот так вот. Если структура реализует IEquatable<T>, то вызов Equals с таким же типом — это прямые погоны, никакой упаковки в объект.

А теперь, как вызвать боксинг и просрать производительность:

Надо быть конченым оптимизатором, чтобы специально так делать, но знать стоит.

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

// Способ 1: Напрямую вызвать унаследованный от object метод.
bool isEqual3 = p1.Equals((object)p2); // А вот тут, блядь, ПРИВЕТ! p2 запакуется в объект (боксинг), потому что вызывается старый `Equals(object)`.

// Способ 2: Сунуть структуру в какой-нибудь древний пиздец типа ArrayList.
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(p1); // Тут при добавлении происходит пиздец — полный боксинг, потому что коллекция работает с object.
// И потом все сравнения через её методы будут с распаковкой-упаковкой, ад и срака.

// Способ 3: Привести к интерфейсу, который структура реализует.
IEquatable<Point> ie = p1; // И тут, внимание, тоже боксинг! Потому что интерфейс — это ссылочный тип.

Итог и мораль, которую надо высечь на жопе:

Боксинг — это когда твоя легковесная структура на стеке тупо заворачивается в объект в куче, со всеми вытекающими: лишняя аллокация, нагрузка на GC. В циклах на миллион итераций — это пиздец, а не оптимизация.

Чтобы этого не было:

  1. Всегда реализуй IEquatable<T> для своих значимых типов.
  2. Используй нормальные, обобщённые коллекции (List<Point>, Dictionary<Point, ...>), а не legacy-уёбища вроде ArrayList или Hashtable.
  3. Сравнивай через == (если перегрузил) или через Equals(Point), а не через приведение к object.

Если будешь следовать этим простым правилам, твой код будет летать, а не ебашить аллокации на ровном месте. Всё, вопрос закрыт.