Rust habilita el polimorfismo a través de objetos de rasgo (dyn Trait), que dependen de vtables para despachar llamadas a métodos en tiempo de ejecución. Estas vtables se generan por implementación y contienen estrictamente punteros a funciones correspondientes a los métodos del rasgo, estableciendo una convención de llamada uniforme entre diferentes tipos concretos.
La inclusión de constantes asociadas (const NAME: Type;) dentro de una definición de rasgo impide la seguridad del objeto porque las constantes son constructos de tiempo de compilación resueltos durante la monomorfización. A diferencia de los métodos, las constantes no ocupan espacios en las vtables, ya que carecen de una representación uniforme en tiempo de ejecución y normalmente están en línea o almacenadas en secciones de datos de solo lectura. Intentar crear un dyn Trait para tal rasgo requeriría que el objeto de rasgo contenga o haga referencia al valor constante dinámicamente, lo cual contradice el diseño arquitectónico de las vtables y el borrado de tipos.
Para resolver esto, la constante debería transformarse en un método (por ejemplo, fn name(&self) -> Type) o en un tipo asociado si el valor representa un tipo. Esta alteración coloca la recuperación del valor detrás de un puntero a función en la vtable, restaurando así la seguridad del objeto mientras se introduce un mínimo costo en tiempo de ejecución.
Al arquitectar una capa de abstracción de hardware para un RTOS embebido, necesitábamos un Registro unificado para gestionar conductores de sensores dispares que implementaran un rasgo Sensor. Cada controlador requería un const DEVICE_ID: u16 único para la asignación del bus I2C, que inicialmente definimos como una constante asociada dentro del rasgo.
El obstáculo inmediato surgió cuando intentamos almacenar sensores heterogéneos en un Vec<Box<dyn Sensor>>, resultando en un error del compilador que citaba la violación de las reglas de seguridad del objeto del rasgo. Esto impedía el despacho dinámico necesario para que el registro pudiera sondear sensores de manera genérica.
Evaluamos tres enfoques. Primero, convertir DEVICE_ID a un método fn device_id(&self) -> u16 permitió que el Vec funcionara correctamente pero incurrió en un costo de búsqueda de vtable y evitó la verificación de direcciones en tiempo de compilación. En segundo lugar, utilizar un registro genérico Vec<Box<T>> donde T: Sensor fue rechazado porque requería almacenamiento homogéneo, eliminando la capacidad de mezclar sensores de temperatura y presión. Tercero, implementar un enum de borrado de tipos manual enum DynSensor { Temp(TempSensor), Press(PressSensor) } preservó las constantes pero nos obligó a modificar el enum para cada nuevo controlador, violando el principio de abierto/cerrado.
Adoptamos la primera solución, aceptando el costo en tiempo de ejecución a cambio de la flexibilidad obtenida. El sistema resultante gestionó con éxito treinta tipos distintos de sensores a través de una única interfaz, aunque documentamos la compensación arquitectónica en las directrices del crate para futuros autores de controladores.
¿Por qué pueden usarse los tipos asociados en objetos de rasgo pero las constantes asociadas no, dado que ambos se resuelven en tiempo de compilación por implementación?
Los tipos asociados están integrados en la identidad del sistema de tipos. Al construir un objeto de rasgo como Box<dyn Trait<AssocType = u32>>, el tipo asociado se convierte en parte de la firma de tipo estática conocida por el compilador en el sitio de creación. La vtable permanece válida porque el tipo concreto (y por lo tanto el tipo asociado) es fijo. Por el contrario, las constantes asociadas son valores, no tipos. Rust carece de sintaxis para dyn Trait<CONST = 5> y las vtables no pueden almacenar valores de datos arbitrarios—solo punteros a funciones—lo que hace que las constantes sean inaccesibles a través del tipo borrado.
¿Podrían los genéricos const en el rasgo permitir que las constantes asociadas funcionen con objetos de rasgo al hacer que la constante sea parte del tipo?
Aplicar genéricos const (por ejemplo, trait Trait<const N: usize>) efectivamente haría que la constante fuera parte del tipo, pero esto excluye colecciones heterogéneas. Cada valor constante distinto instancia un tipo de rasgo distinto, lo que significa que Box<dyn Trait<1>> y Box<dyn Trait<2>> son tipos incompatibles almacenados en diferentes contenedores Vec. Este enfoque sacrifica la capacidad del contenedor polimórfico que motiva el uso de objetos de rasgo, haciéndolo inapropiado para registros que requieren implementaciones mixtas.
¿Cómo impacta la ausencia de constantes asociadas en objetos de rasgo patrones como registros de fábricas o sistemas de complementos que dependen de metadatos?
Los desarrolladores a menudo intentan iterar sobre Vec<Box<dyn Plugin>> para filtrar por un const VERSION: &str asociado, solo para descubrir que los metadatos están borrados. La solución implica o bien incrustar metadatos junto al objeto de rasgo en una estructura de envoltura (por ejemplo, struct PluginEntry { meta: Metadata, plugin: Box<dyn Plugin> }) o usar TypeId y downcasting con Any para recuperar el tipo concreto y acceder a sus constantes. Este último requiere límites 'static y anula los beneficios de abstracción del objeto de rasgo, enfatizando que los objetos de rasgo deliberadamente intercambian información en tiempo de compilación por dinamismo en tiempo de ejecución.