История. Ранний Rust требовал, чтобы все типы имели статически известный размер для гарантии стековой аллокации и эффективной семантики значений. Когда были введены динамически размеченные типы (DSTs), такие как срезы [T] и объекты трейт dyn Trait, чтобы поддерживать гибкие структуры данных, языку потребовался механизм для различия между обобщенными параметрами фиксированного и потенциально нефиксированного размера, не ломая существующий код. Синтаксис ?Sized был принят как «ослабленное» ограничение, позволяющее обобщениям явно отказаться от требования по умолчанию Sized, при этом сохраняя эргономичное поведение по умолчанию для большинства случаев использования, не связанных с нефиксированными данными.
Проблема. Неявное ограничение T: **Sized** создает фундаментальное напряжение: оно позволяет манипулировать значениями и проводить расчет памяти на этапе компиляции, но предотвращает использование функций с dyn Trait или типами срезов напрямую без косвенности. Это ограничение заставляет разработчиков использовать Box или ссылки даже тогда, когда желательна семантика владения, что усложняет API, которые стремятся поддерживать как статическую, так и динамическую полиморфию. Без ?Sized обобщенный код не может абстрагироваться как над конкретными типами, так и над полиморфными объектами времени выполнения, что приводит либо к вынужденной аллокации в куче, либо к дублированию интерфейсов для фиксированных и нефиксированных вариантов.
Решение. Компилятор решает эту проблему, заставляя типы с ограничением ?Sized можно получать только через жирные указатели — составные значения, содержащие указатель на данные и метаданные времени выполнения (длина для срезов, таблица виртуальных функций для объектов трейт). Когда обобщенное определение специфицирует T: **?Sized**, компилятор запрещает операции, требующие известных размеров, такие как std::mem::size_of::<T>() или перемещение значений по значению, обеспечивая, чтобы все макеты памяти оставались вычисляемыми на этапе компиляции. Этот дизайн позволяет нулевые издержки абстракций, где фиксированные типы используют тонкие указатели, а нефиксированные типы используют жирные указатели, при этом система типов прозрачно обрабатывает различия.
Библиотека мониторинга систем нуждалась в том, чтобы регистрировать ошибки, которые могли быть либо небольшими, размещенными в стеке кодами ошибок, либо большими, динамически форматированными сообщениями об ошибках, реализующими dyn **Display**. Первоначальный дизайн API, использующий fn log<T: **Display**>(error: T), отклонил объекты трейтов, потому что неявное ограничение Sized предотвращало выполнение dyn Display, создавая значительное эргономическое препятствие для динамической обработки ошибок.
Первый рассмотренный подход заключался в том, чтобы требовать Box<dyn **Display**> для всех типов ошибок, конвертируя даже простые коды ошибок u32 в аллокации в куче. Плюсы: унификация поверхности API и разрешение владения динамическими ошибками без сложных обобщений. Минусы: ввод зависимостей от распределителей, ненадлежащих для встроенных целей, и добавление измеримой задержки к горячим путям обработки простых, статических ошибок.
Второй вариант заключался в поддержании двух отдельных методов логгирования: одного для обобщенных типов T: **Display** фиксированного размера и одного, специально для &dyn **Display**. Плюсы: избегание аллокации в куче для фиксированных типов и правильная поддержка динамической диспетчеризации для сложных ошибок. Минусы: требовали значительного дублирования кода, усложняли документацию API и принуждали вызывающих выбирать правильный метод, основываясь на знании размера типа.
Команда выбрала третий подход, используя fn log<T: **?Sized** + **Display**>(error: &T), принимая ссылки на как фиксированные, так и нефиксированные типы. Это решение было выбрано, потому что оно сохраняло единую, четкую точку входа в API, поддерживало среды no-std, избегая обязательного бокса, и накладывало нулевые накладные расходы времени выполнения по сравнению с подходом с двумя методами. Генерируемая обобщенная реализация компилировалась в тот же машинный код для фиксированных типов, что и оригинальная мономорфная версия, в то время как обработка объектов трейтов происходила через диспетчеризацию таблицы виртуальных функций.
В результате полученный crate успешно развертывался на микроконтроллерах и серверах, обрабатывая миллионы разнородных событий ошибок без накладных расходов на аллокацию. Унифицированный интерфейс позволял разработчикам бесшовно передавать как &ConcreteError, так и &dyn Error, демонстрируя, что ?Sized действительно позволяет добиться нулевых затрат на полиморфизм через разнообразные целевые развертывания.
Почему функция не может возвращать значение типа T, где T: **?Sized**?
Функции, возвращающие значения, должны размещать эти значения в регистрах или на стеке, что требует статически известного размера для генерации правильного кода конвенции вызова и резервирования соответствующего пространства в стеке. Поскольку типы ?Sized, такие как [i32] или dyn **Debug**, имеют размеры, определяемые во время выполнения, компилятор не может сгенерировать последовательности команд возврата фиксированного размера, необходимые для ABI. Только типы указателей (Box<T>, &T) имеют статически известные размеры (ширина usize или жирного указателя), что делает их единственными законными типами возврата для нефиксированных данных, существенно ограничивая обобщенные типы ?Sized до «типов просмотра», а не «типов значений», которые могут быть перемещены по значению.
Как **?Sized** взаимодействует с правилами согласованности, касающимися реализаций трейт для ссылок?
При реализации трейтов для &T, где T: **?Sized**, реализация автоматически применяется к жирным указателям (таким как &[i32] или &dyn Trait), поскольку это просто ссылки на типы ?Sized. Кандидаты часто упускают, что impl Trait for &T where T: **?Sized** охватывает как тонкие, так и жирные указатели, тогда как impl Trait for T where T: **Sized** этого не делает. Это различие имеет важное значение для определения обобщенных реализаций, которые работают одновременно с фиксированными данными и объектами трейтов, обеспечивая согласованность по всей иерархии типов без перекрывающихся реализаций, которые нарушают правила сирот Rust.
Что отличает представление памяти **Box<dyn Trait>** от **&dyn Trait** помимо семантики владения?
Хотя оба используют жирные указатели (указатель + таблица виртуальных функций), **Box<dyn Trait>** владеет выделением и хранит указатель на таблицу виртуальных функций в частности для целей освобождения памяти, в то время как **&dyn Trait** просто наблюдает за данными. Критически важно, что Box<T>, где T: **?Sized** требует, чтобы распределитель обрабатывал динамическое освобождение памяти с использованием размера, хранящегося в таблице виртуальных функций, в то время как ссылки не несут такой ответственности. Начинающие часто упускают, что Box позволяет аллокацию в куче нефиксированных типов, которые не могут существовать в стеке, тогда как ссылки просто заимствуют существующую память, делая Box необходимым для возврата владения нефиксированными данными из функций.