Rust обеспечивает полиморфизм через трейтовые объекты (dyn Trait), которые полагаются на vtable для выполнения вызовов методов во время выполнения. Эти vtable генерируются для каждой реализации и строго содержат указатели функций, соответствующие методам трейта, устанавливая единый порядок вызова для различных конкретных типов.
Включение ассоциированных констант (const NAME: Type;) в определение трейта препятствует безопасности объектов, поскольку константы являются конструкциями времени компиляции, разрешаемыми во время мономорфизации. В отличие от методов, константы не занимают слоты в vtable, так как у них отсутствует единое представление во время выполнения, и обычно они инлайнятся или хранятся в разделах данных только для чтения. Попытка создать dyn Trait для такого трейта потребовала бы от объектного типа передавать или ссылаться на значение константы динамически, что противоречит архитектурному дизайну vtable и стиранию типов.
Чтобы разрешить это, константу следует преобразовать в метод (например, fn name(&self) -> Type) или ассоциированный тип, если значение представляет собой тип. Это изменение помещает извлечение значения за указатель функции в vtable, тем самым восстанавливая безопасность объекта при минимальных накладных расходах во время выполнения.
При проектировании слоя абстракции аппаратного обеспечения для встроенной RTOS нам нужен был единый Реестр для управления разными драйверами датчиков, реализующими трейт Sensor. Каждый драйвер требовал уникальный const DEVICE_ID: u16 для адресации шины I2C, который мы изначально определили как ассоциированную константу в трейте.
Непосредственная проблема возникла при попытке сохранить разнородные датчики в Vec<Box<dyn Sensor>>, что привело к ошибке компилятора, ссылающейся на нарушение правил безопасности объектов трейта. Это предотвратило динамическую диспетчеризацию, необходимую для того, чтобы реестр мог опрашивать датчики в общем виде.
Мы оценили три подхода. Во-первых, преобразование DEVICE_ID в метод fn device_id(&self) -> u16 позволило Vec работать правильно, но повлекло за собой штраф на поиск vtable и предотвратило проверку адреса на этапе компиляции. Во-вторых, использование обобщенного реестра Vec<Box<T>>, где T: Sensor, было отклонено, так как требовало однородного хранения, исключая возможность совместного использования датчиков температуры и давления. В-третьих, реализация вручную стираемого типа коллекции enum DynSensor { Temp(TempSensor), Press(PressSensor) } сохранила бы константы, но заставила бы нас изменять перечисление для каждого нового драйвера, нарушая принцип открытости/закрытости.
Мы выбрали первое решение, приняв во внимание накладные расходы на время выполнения ради полученной гибкости. Полученная система успешно управляла тридцатью различными типами датчиков через единый интерфейс, хотя мы задокументировали архитектурный компромисс в руководстве пакета для будущих авторов драйверов.
Почему ассоциированные типы могут использоваться в трейтовых объектах, а ассоциированные константы нет, учитывая, что оба разрешаются на этапе компиляции для каждой реализации?
Ассоциированные типы интегрируются в идентичность типовой системы. При создании трейтового объекта, такого как Box<dyn Trait<AssocType = u32>>, ассоциированный тип становится частью статической сигнатуры типа, известной компилятору на этапе создания. vtable остается действительным, поскольку конкретный тип (и, следовательно, ассоциированный тип) фиксирован. Напротив, ассоциированные константы — это значения, а не типы. Rust не имеет синтаксиса для dyn Trait<CONST = 5>, и vtable не может хранить произвольные значения данных — только указатели функций — что делает константы недоступными через стертый тип.
Могли бы константные параметры в трейте позволить ассоциированным константам работать с трейтовыми объектами, сделав константу частью типа?
Применение константных параметров (например, trait Trait<const N: usize>) действительно сделало бы константу частью типа, но это исключает разнородные коллекции. Каждое различное значение константы создает отдельный тип трейта, что означает, что Box<dyn Trait<1>> и Box<dyn Trait<2>> несовместимы как типы, хранящиеся в разных контейнерах Vec. Этот подход жертвует возможностью полиморфной контейнеризации, что и мотивирует использование трейтовых объектов, что делает его неприемлемым для реестров, требующих смешанных реализаций.
Как отсутствие ассоциированных констант в трейтовых объектах влияет на такие паттерны, как фабричные реестры или системы плагинов, которые зависят от метаданных?
Разработчики часто пытаются выполнять итерацию по Vec<Box<dyn Plugin>>, чтобы фильтровать по ассоциированной const VERSION: &str, только чтобы обнаружить, что метаданные стерты. Решение включает либо встраивание метаданных вместе с трейтовым объектом в структуру-обертку (например, struct PluginEntry { meta: Metadata, plugin: Box<dyn Plugin> }), либо использование TypeId и Any для обратного преобразования для восстановления конкретного типа и доступа к его константам. Последнее требует ограничений 'static и отменяет преимущества абстракции объектного трейта, подчеркивая, что трейтовые объекты сознательно жертвуют информацией времени компиляции ради динамичности во время выполнения.