Rust umożliwia polimorfizm poprzez obiekty cech (dyn Trait), które polegają na vtables do wywoływania metod w czasie wykonania. Te vtables są generowane dla każdego wdrożenia i zawierają wyłącznie wskaźniki do funkcji odpowiadające metodom cechy, tworząc jednolitą konwencję wywołania dla różnych typów konkretnych.
Włączenie związanych stałych (const NAME: Type;) w definicji cechy uniemożliwia bezpieczeństwo obiektu, ponieważ stałe są konstrukcjami czasów kompilacji, które są rozwiązywane podczas monomorfizacji. W przeciwieństwie do metod, stałe nie zajmują slotów w vtables, ponieważ nie mają jednolitej reprezentacji czasów wykonania i zazwyczaj są w inline’owane lub przechowywane w sekcjach danych tylko do odczytu. Próba stworzenia dyn Trait dla takiej cechy wymagałaby, aby obiekt cechy przenosił lub odnosił się do wartości stałej dynamicznie, co stoi w sprzeczności z architektonicznym projektem vtables i wymazywaniem typów.
Aby to rozwiązać, stała powinna zostać przekształcona w metodę (np. fn name(&self) -> Type) lub typ związany, jeśli wartość reprezentuje typ. Ta zmiana umieszcza pobieranie wartości za wskaźnikiem funkcji w vtable, przywracając bezpieczeństwo obiektu przy minimalnym narzucie czasów wykonania.
Podczas projektowania warstwy abstrahującej sprzęt dla osadzonego RTOS potrzebowaliśmy jednolitego Rejestru do zarządzania różnymi sterownikami sensorów implementującymi cechę Sensor. Każdy sterownik wymagał unikalnego const DEVICE_ID: u16 do adresowania magistrali I2C, które pierwotnie zdefiniowaliśmy jako związaną stałą w obrębie cechy.
Natychmiastową przeszkodą okazało się próba przechowywania heterogenicznych sensorów w Vec<Box<dyn Sensor>>, co skutkowało błędem kompilatora, który wskazywał na naruszenie zasad bezpieczeństwa obiektów przez cechę. To uniemożliwiło dynamiczne wywołanie konieczne dla rejestru do ogólnego odpytywania sensorów.
Oceniliśmy trzy podejścia. Po pierwsze, konwersja DEVICE_ID na metodę fn device_id(&self) -> u16 umożliwiła działanie Vec, ale wiązała się z naliczeniem opłaty za wyszukiwanie vtable i uniemożliwiła weryfikację adresów w czasie kompilacji. Po drugie, wykorzystanie ogólnego rejestru Vec<Box<T>>, gdzie T: Sensor, zostało odrzucone, ponieważ wymagało jednorodnego przechowywania, eliminując możliwość łączenia czujników temperatury i ciśnienia. Po trzecie, wdrożenie manualnej wymazywania typów typu enum enum DynSensor { Temp(TempSensor), Press(PressSensor) } zachowało by stałe, ale zmuszało nas do modyfikacji enumu dla każdego nowego sterownika, naruszając zasadę otwarte/zamknięte.
Przyjęliśmy pierwsze rozwiązanie, akceptując koszt czasów wykonania dla uzyskania elastyczności. Ostateczny system skutecznie zarządzał trzydziestoma różnymi typami sensorów przez pojedynczy interfejs, chociaż udokumentowaliśmy kompromis architektoniczny w wytycznych dla przyszłych autorów sterowników w crate.
Dlaczego typy związane mogą być używane w obiektach cech, ale związane stałe nie mogą, skoro oba są rozwiązywane w czasie kompilacji dla każdego wdrożenia?
Typy związane są zintegrowane z tożsamością systemu typów. Podczas konstruowania obiektu cechy, takiego jak Box<dyn Trait<AssocType = u32>>, związany typ staje się częścią statycznego podpisu typowego znanego kompilatorowi w miejscu tworzenia. vtable pozostaje ważny, ponieważ typ konkretny (a tym samym związany typ) jest ustalony. Z drugiej strony, związane stałe to wartości, a nie typy. Rust nie posiada składni dla dyn Trait<CONST = 5> i vtables nie mogą przechowywać dowolnych wartości danych — tylko wskaźniki do funkcji — co sprawia, że stałe są niedostępne przez wymazany typ.
Czy ogólne const w cesze mogłyby umożliwić działanie związanych stałych z obiektami cech, czyniąc wartość stałą częścią typu?
Zastosowanie ogólnych const (np. trait Trait<const N: usize>) faktycznie uczyniłoby stałą częścią typu, ale wyklucza to kolekcje heterogeniczne. Każda odmienna wartość stała instancjonuje odrębny typ cechy, co oznacza, że Box<dyn Trait<1>> i Box<dyn Trait<2>> to niekompatybilne typy przechowywane w różnych kontenerach Vec. To podejście poświęca zdolność pojemnika polimorficznego, która motywuje użycie obiektów cech, czyniąc je nieodpowiednimi dla rejestrów wymagających mieszanych implementacji.
Jak brak związanych stałych w obiektach cech wpływa na wzorce, takie jak rejestry fabryczne lub systemy wtyczek, które polegają na metadanych?
Programiści często próbują iterować przez Vec<Box<dyn Plugin>>, aby filtrować według związanej const VERSION: &str, tylko po to, by odkryć, że metadane zostały wymazane. Rozwiązaniem jest albo osadzenie metadanych obok obiektu cechy w strukturze opakowującej (np. struct PluginEntry { meta: Metadata, plugin: Box<dyn Plugin> }), albo użycie TypeId i Any do zrzutu typów, aby odzyskać typ konkretny i uzyskać dostęp do jego stałych. To ostatnie wymaga ograniczeń 'static i neguje korzyści związane z abstrakcją obiektu cechy, podkreślając, że obiekty cech celowo wymieniają informacje czasów kompilacji na dynamikę czasów wykonania.