Comments 4
у вас в проде нашёлся самый неожиданный боксинг
Самый неожиданный боксинг был по поведению для nullable-типов в современном C#. Так приведение nullable-типа к object при боксинге меняется тип. Например, при приведении Int32? varialble к object боксинг произойдёт только в том случае, если variable был не null. Если был null, то боксинга не будет, просто переменной типа object присвоится null и сведения о изначальном типе потеряются и потом при анбоксинге можно будет привести это значение хоть к Boolean?.
Как только
structприсваивается переменной интерфейсного типа, он боксится — ведь интерфейс ссылочный. Вызовs.Area()идёт уже по упакованной копии. Если вы держитеCircleв локальной переменной конкретного типа и вызываетеArea()напрямую — боксинга нет. Грабли вылезают, когдаstruct“прячут” за интерфейс ради полиморфизма.Лечение: generic-ограничение
where T : struct, IShapeвместо переменной интерфейса — JIT специализирует код по value-типу и вызывает метод без упаковки.
Ссылочность вообще не причем. Структуры прекрасно передаются по ссылке без всяких аллокаций. Проблема в том что каждая передаваемая переменная должна иметь фиксированный заранее известный размер. Если переменная интерфейсного типа то размер фактически передаваемых данных может быть любым до 0 и далее. Не может быть переменной или параметра, размер которого никто не знает при исполнении. В отличии от ссылки размер которой всегда фиксированный. Боксинг это способ впихнуть невпихуемое.
Когда мы пишем ограничение where T : struct, IShape то это совсем другая история, так как это скрывает вызов отдельной предварительно сгенерированной функции для каждого используемого типа. У каждой функции будет свой параметр фиксированного размера.
Боксинг, который уже НЕ боксит: чему научился рантайм
Generic-методы со
struct-аргументом не боксят сам аргумент.EqualityComparer<T>.Default,List<T>,Dictionary<TKey,TValue>специализируются по value-типу: JIT генерирует отдельный нативный код на каждыйstruct-аргумент, и упаковки value→object там нет (при условии корректногоIEquatable<T>— см. пункт 3).
Что значит уже? В C# изначально мономорфизация параметров типов что значит что любой вариант типа с параметром мономорфизируеться в отдельный тип, там нечему бокситься и незачем. Другими словами, не было никакого List<int> всегда был List_int.
У вас, всё-таки, ошибка в методе лечения.
>Лечение: generic-ограничение where T : struct, IShape вместо переменной интерфейса — JIT специализирует код по value-типу и вызывает метод без упаковки.
struct в ограничении параметра интерфейса совсем необязательно указывать. Важно сделать аргументом метода не интерфейс, а generic-тип, на который наложено ограничение имплементировать нужный интерфейс. Тогда, как писал ещё Джеффри Рихтера, C# для reference-типов создаст одну общую имплементацию этого метода, а для каждого структурного типа сделает отдельную имплементацию. Эта отдельная имплементация будет знать конкретный тип параметра и не будет боксинга.
Вот ваш немного переделанный пример на sharplab. Ограничения struct нет, а боксинг лечится.
https://sharplab.io/#gist:a9fa36483d3aa22fb79b12b3704efac6
using System;
public interface IShape { double Area(); }
public struct Circle : IShape
{
public double R;
public double Area() => Math.PI * R * R;
}
public class C {
public double GetDoubleAreaWithBoxing() {
IShape s = new Circle { R = 2 };
return s.Area();
}
public double GetDoubleAreaWithoutBoxing() {
Circle circle = new Circle { R = 2 };
return GetShapeDoubleArea(circle);
}
public double GetShapeDoubleArea<T>(T shape) where T: IShape {
var area = shape.Area();
return area * 2;
}
}
Аллокации, которых нет в коде: охота на скрытый боксинг в .NET 10