Rust는 런타임에 메소드 호출을 전송하기 위해 vtables에 의존하는 trait objects (dyn Trait)을 통해 다형성을 가능하게 합니다. 이 vtables는 구현별로 생성되며 특성의 메소드에 해당하는 함수 포인터만을 엄격하게 포함하여 다양한 구체적 유형에 걸쳐 균일한 호출 규약을 설정합니다.
특성 정의 내의 연결된 상수(const NAME: Type;)의 포함은 상수들이 단일 전개 시 해석되는 컴파일 타임 구조물이기 때문에 객체 안전성을 방해합니다. 메소드와 달리, 상수는 vtables에서 슬롯을 차지하지 않으며, 균일한 런타임 표현이 부족하고 일반적으로 인라인되거나 읽기 전용 데이터 섹션에 저장됩니다. 그러한 특성에 대해 dyn Trait을 생성하려고 하면, 특성 객체가 동적으로 상수 값을 포함하거나 참조해야 하므로 vtables 및 타입 지우기의 아키텍처 설계와 모순됩니다.
이를 해결하기 위해 상수는 메소드(예: fn name(&self) -> Type) 또는 그 값이 유형을 나타내는 경우 연결된 유형으로 변환되어야 합니다. 이 변경은 값 검색을 vtable의 함수 포인터 뒤에 위치시키며, 객체 안전성을 회복하면서 최소한의 런타임 오버헤드를 추가합니다.
임베디드 RTOS를 위한 하드웨어 추상화 계층을 설계하는 과정에서, 우리는 다양한 센서 드라이버가 구현하는 Sensor 특성을 관리하기 위한 통합 Registry가 필요했습니다. 각 드라이버는 I2C 버스 주소 지정에 대해 고유한 const DEVICE_ID: u16가 필요했습니다. 우리는 이를 처음에 특성 내의 연결된 상수로 정의했습니다.
즉시 장애물이 발생했습니다. 이질적인 센서를 Vec<Box<dyn Sensor>>에 저장하려고 시도할 때, 컴파일러가 특성이 객체 안전성 규칙을 위반했다고 인용하는 오류가 발생했습니다. 이것은 레지스트리가 센서를 일반적으로 요청하는 데 필요한 동적 전송을 방해했습니다.
우리는 세 가지 접근 방식을 평가했습니다. 첫 번째로, DEVICE_ID를 메소드 fn device_id(&self) -> u16로 변환하여 Vec이 제대로 작동하도록 했지만, vtable 조회 비용이 발생하고 컴파일 타임 주소 검증이 불가능해졌습니다. 두 번째로, T: Sensor인 제너릭 레지스트리 Vec<Box<T>>를 사용하는 것은 수용할 수 없었습니다. 이는 동종 저장소를 요구하여 온도 및 압력 센서를 섞을 수 있는 능력을 없앴습니다. 세 번째로, 수동 타입 지우기 열거형 enum DynSensor { Temp(TempSensor), Press(PressSensor) }을 구현하였으나, 이는 상수를 유지하면서도 새로운 드라이버마다 열거형을 수정해야 했기 때문에 개방/폐쇄 원칙을 위반하게 되었습니다.
우리는 첫 번째 솔루션을 채택하고, 얻은 유연성에 대한 런타임 비용을 수용했습니다. 결과적으로 이 시스템은 단일 인터페이스를 통해 서른 개의 다양한 센서 유형을 성공적으로 관리했으며, 미래의 드라이버 저자들을 위한 크레이트의 지침서에 아키텍처적 트레이드오프를 문서화했습니다.
둘 다 구현당 컴파일 타임에 해석되지만, 왜 연결된 유형은 특성 객체에서 사용될 수 있지만, 연결된 상수는 사용할 수 없는가?
연결된 유형은 타입 시스템의 정체성과 통합됩니다. Box<dyn Trait<AssocType = u32>>와 같은 특성 객체를 구성할 때, 연결된 유형은 생성 위치에서 컴파일러가 아는 정적 타입 서명 일부가 됩니다. vtable은 구체적 유형(따라서 연결된 유형)이 고정되어 있으므로 유효성을 유지합니다. 반면에, 연결된 상수는 값이지 유형이 아닙니다. Rust에는 dyn Trait<CONST = 5>라는 구문이 없으며, vtables는 임의의 데이터 값을 저장할 수 없습니다. 오직 함수 포인터만 저장할 수 있기 때문에, 상수는 지워진 타입을 통해 접근할 수 없습니다.
특성에서 const 제너릭을 적용하면, 연결된 상수가 타입의 일부가 되어 특성 객체와 함께 작동할 수 있게 될까?
const 제너릭을 적용하면(예: 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 경계가 필요하며, 특성 객체의 추상화 이점을 부정하므로, 특성 객체는 의도적으로 컴파일 타임 정보를 런타임 동적성으로 대체하는 것을 강조합니다.