Rust ermöglicht Polymorphismus durch Trait-Objekte (dyn Trait), die auf vtables angewiesen sind, um Methodenaufrufe zur Laufzeit zu dispatchen. Diese vtables werden pro Implementierung generiert und enthalten ausschließlich Funktionszeiger, die den Methoden des Traits entsprechen, wodurch eine einheitliche Calling Convention über verschiedene konkrete Typen hinweg geschaffen wird.
Die Einbeziehung von assoziierten Konstanten (const NAME: Type;) in eine Trait-Definition verhindert die Objektsicherheit, da Konstanten zur Compile-Zeit gelöst werden, während der Monomorphisierung. Im Gegensatz zu Methoden belegen Konstanten keine Slots in vtables, da sie keine einheitliche Laufzeitdarstellung haben und normalerweise inline oder in schreibgeschützten Datenbereichen gespeichert werden. Der Versuch, ein dyn Trait für ein solches Trait zu erstellen, würde erfordern, dass das Trait-Objekt den konstanten Wert dynamisch trägt oder referenziert, was im Widerspruch zu dem architektonischen Design von vtables und Typauslöschung steht.
Um dies zu lösen, sollte die Konstante in eine Methode umgewandelt werden (z. B. fn name(&self) -> Type) oder in einen assoziierten Typ, wenn der Wert einen Typ darstellt. Diese Änderung platziert den Wertabruf hinter einem Funktionszeiger in der vtable und stellt somit die Objektsicherheit wieder her, während der Runtime-Overhead minimal bleibt.
Bei der Architektur einer Hardware-Abstraktionsschicht für ein eingebettetes RTOS benötigten wir ein einheitliches Registry, um verschiedene Sensor-Treiber zu verwalten, die ein Sensor-Trait implementieren. Jeder Treiber benötigte eine eindeutige const DEVICE_ID: u16 für die I2C-Bus-Adressen, die wir zunächst als assoziierte Konstante im Trait definiert hatten.
Das unmittelbare Hindernis trat auf, als wir versuchten, heterogene Sensoren in einem Vec<Box<dyn Sensor>> zu speichern, was zu einem Compilerfehler führte, der das Verstoßen gegen die Objektsicherheitsregeln des Traits anprangerte. Dies verhinderte das dynamische Dispatching, das erforderlich war, damit das Registry Sensoren generisch abfragen konnte.
Wir evaluierten drei Ansätze. Erstens ermöglichte die Umwandlung von DEVICE_ID in eine Methode fn device_id(&self) -> u16 das ordnungsgemäße Funktionieren des Vec, verursachte jedoch einen vtable-Lookup-Strafaufwand und verhinderte eine Überprüfung der Adresse zur Compile-Zeit. Zweitens wurde die Verwendung eines generischen Registrys Vec<Box<T>>, wobei T: Sensor abgelehnt, da es eine homogene Speicherung erforderte, was die Fähigkeit zur Mischung von Temperatur- und Drucksensoren ausschloss. Drittens machte die Implementierung eines manuellen Typauslöschungs-Enums enum DynSensor { Temp(TempSensor), Press(PressSensor) } die Konstanten gültig, zwang uns jedoch dazu, das Enum für jeden neuen Treiber zu ändern, was das offene/geschlossene Prinzip verletzte.
Wir nahmen die erste Lösung an und akzeptierten die Laufzeitkosten für die gewonnene Flexibilität. Das resultierende System verwaltete erfolgreich dreißig verschiedene Sensortypen über eine einzige Schnittstelle, obwohl wir den architektonischen Kompromiss in den Richtlinien des Crates für zukünftige Treiberautoren dokumentierten.
Warum können assoziierte Typen in Trait-Objekten verwendet werden, während assoziierte Konstanten dies nicht können, obwohl beide zur Compile-Zeit pro Implementierung aufgelöst werden?
Assoziierte Typen sind in die Identität des Typsystems integriert. Wenn ein Trait-Objekt wie Box<dyn Trait<AssocType = u32>> konstruiert wird, wird der assoziierte Typ Teil der statischen Typsignatur, die dem Compiler am Erstellungsort bekannt ist. Die vtable bleibt gültig, da der konkrete Typ (und damit der assoziierte Typ) festgelegt ist. Im Gegensatz dazu sind assoziierte Konstanten Werte, keine Typen. Rust hat keine Syntax für dyn Trait<CONST = 5> und vtables können keine willkürlichen Datenwerte speichern – nur Funktionszeiger –, was Konstanten über den ausgelöschten Typ unzugänglich macht.
Könnten konstante Generika im Trait es ermöglichen, dass assoziierte Konstanten mit Trait-Objekten funktionieren, indem die Konstante Teil des Typs gemacht wird?
Die Anwendung von konstanten Generika (z. B. trait Trait<const N: usize>) würde in der Tat die Konstante Teil des Typs machen, aber dies schließt heterogene Sammlungen aus. Jeder unterschiedliche konstante Wert instanziiert einen unterschiedlichen Trait-Typ, was bedeutet, dass Box<dyn Trait<1>> und Box<dyn Trait<2>> inkompatible Typen sind, die in verschiedenen Vec-Containern gespeichert sind. Dieser Ansatz opfert die Fähigkeit des polymorphen Containers, die die Verwendung von Trait-Objekten motiviert, wodurch er für Registrys, die gemischte Implementierungen erfordern, ungeeignet wird.
Wie wirkt sich das Fehlen von assoziierten Konstanten in Trait-Objekten auf Muster wie Fabrikregister oder Plug-in-Systeme aus, die auf Metadaten angewiesen sind?
Entwickler versuchen häufig, über Vec<Box<dyn Plugin>> zu iterieren, um nach einer assoziierten const VERSION: &str zu filtern, nur um festzustellen, dass die Metadaten ausgelöscht sind. Die Lösung besteht darin, entweder die Metadaten zusammen mit dem Trait-Objekt in einer Wrapper-Struktur einzubetten (z. B. struct PluginEntry { meta: Metadata, plugin: Box<dyn Plugin> }) oder TypeId und Any zum Heruntercasten zu verwenden, um den konkreten Typ zurückzugewinnen und auf seine Konstanten zuzugreifen. Letzteres erfordert 'static-Bounds und negiert die Abstraktionsvorteile des Trait-Objekts, was betont, dass Trait-Objekte bewusst Compile-Zeitinformationen gegen Laufzeitdynamik eintauschen.