Что меняется в обобщённой функции после добавления ограничения ?Sized к параметру типа?
По умолчанию обобщённый параметр типа в Rust имеет неявное ограничение Sized: компилятор предполагает, что размер значения известен на этапе компиляции. Ограничение ?Sized снимает только это требование и позволяет использовать типы с динамически известным размером, например срезы и трейтовые объекты.
Такой параметр обычно нужно использовать за косвенной ссылкой или другим указателем, потому что передать значение нес Sized-типа непосредственно невозможно. При этом остальные bounds сохраняются: T: ?Sized + Trait по-прежнему требует реализацию Trait.
Rust поддерживает типы с динамически известным размером, или DST: например, [T], str и dyn Trait. Их размер нельзя выразить одним фиксированным числом во время компиляции, поэтому такие значения обычно существуют за указателем, ссылкой или контейнером вроде Box.
Для обычных generic-параметров выбран безопасный и удобный default: считать тип Sized. Это позволяет функциям и структурам работать со значениями напрямую без дополнительного анализа способа хранения. ?Sized нужен там, где API намеренно должен принимать также DST.
Рассмотрим обобщённую функцию, принимающую ссылку на тип, реализующий некоторый трейт. Интуитивно ссылка уже содержит всю необходимую информацию для работы с типом неизвестного размера, но параметр T всё равно получает неявный bound Sized.
Если этот bound не снять, функция не примет &dyn Trait или ссылку на срез, даже если сама операция требует только заимствования. Ошибка особенно часто появляется в библиотечных API: ограничение Sized случайно сужает множество допустимых типов и не даёт переиспользовать функцию для DST.
Запись T: Trait фактически означает T: Sized + Trait, если явно не указано обратное. Запись T: ?Sized + Trait разрешает T быть как обычным sized-типом, так и DST.
Вызов measure(&bytes) выводит T как [u8], а measure(object) — как dyn Metric. Оба типа не обязаны иметь статически известный размер, но ссылка содержит необходимую информацию для доступа к значению; для dyn Metric это, в частности, указатель на данные и vtable.
?Sized не означает «тип может быть любым». Тип всё ещё должен реализовывать остальные bounds, а операции внутри функции должны быть совместимы с неизвестным размером. Например, нельзя переместить значение T напрямую или создать локальную переменную типа T без дополнительной гарантии Sized.
Ограничение нужно ставить именно на параметр типа, который допускает DST. Другие параметры могут по-прежнему оставаться Sized. Также ?Sized снимает только неявный bound Sized; оно не превращает T в динамический тип и не включает динамическую диспетчеризацию само по себе.
Главный компромисс — более широкий API против меньшего числа допустимых операций. Если функция работает с владением значением, вычисляет его размер или хранит его непосредственно, Sized обычно необходим. Если функция лишь заимствует значение и вызывает методы трейта, ?Sized часто делает интерфейс более универсальным без потери статической типобезопасности.
В библиотеке есть функция измерения объекта по трейту. Первый вариант оставляет неявный Sized: он прост, но принимает только типы с известным размером и не работает с dyn Metric или [u8].
Второй вариант может принимать T: ?Sized + Metric по ссылке. Он расширяет совместимость с DST и сохраняет статическую проверку трейтовых bounds. Цена — реализация функции должна избегать операций, требующих знания размера или владения значением T.
Третий вариант — принимать сразу &dyn Metric. Он подходит, если нужна только динамическая диспетчеризация, но теряет универсальность для сценариев, где важен конкретный тип или требуется статический dispatch. Кроме того, вызов через vtable обычно имеет косвенный вызов и не предоставляет тех же возможностей оптимизации, что generic-вариант.
Для библиотечной функции, которая только заимствует объект и вызывает методы, выбран вариант с T: ?Sized + Metric. Он поддерживает обычные типы, срезы и трейтовые объекты, сохраняя возможность статической диспетчеризации для конкретных типов и не заставляя всех пользователей платить за динамический dispatch.
?Sized, чтобы функция начала принимать трейтовые объекты?Нет. ?Sized лишь разрешает тип неизвестного размера. Для dyn Trait должны дополнительно выполняться условия использования трейта как объектного: методы должны быть совместимы с object safety, а сам тип должен удовлетворять нужному bound. Например, generic-метод трейта может сделать его непригодным для dyn Trait, и ?Sized этого не исправит.
T: ?Sized обычно передают по ссылке, а не по значению?Размер значения T может быть неизвестен на этапе компиляции, поэтому компилятор не может сформировать обычное место хранения и соглашение о передаче такого значения по значению. Ссылка, Box<T> или другой подходящий указатель имеют известный размер, даже если объект за ними — DST.
Это не означает, что любой указатель автоматически разрешает любой DST: конкретный тип указателя и операция должны поддерживать такой сценарий. Например, Box<dyn Trait> владеет трейтовым объектом, а &dyn Trait только заимствует его.
?Sized способ диспетчеризации вызова метода?Нет. ?Sized регулирует допустимость типов по размеру, а не выбор между static и dynamic dispatch. Если T выведен как конкретный тип, generic-код обычно мономорфизируется и вызов может быть статически разрешён. Если T — dyn Trait, вызов метода выполняется через vtable.
Поэтому одна и та же функция с ?Sized может поддерживать оба сценария. Выбор dispatch определяется фактическим типом, способом передачи и использованием трейтового объекта, а не самим наличием ?Sized.