Estou escrevendo um Rust crate onde tenho uma struct cujo campo implementa uma característica, e gostaria de fornecer um recurso onde:
#[cfg(not(feature = "dyn"))]
: o campo struct éT
despachado estaticamente (mais eficiente, mas menos flexível em tempo de execução)#[cfg(feature = "dyn")]
: struct field éBox<dyn MyTrait>
um objeto de característica despachado dinamicamente (flexibilidade de tempo de execução com pequeno custo de desempenho)
trait MyTrait {}
#[cfg(not(feature = "dyn"))]
struct MyStruct<T: MyTrait> {
my_field: T
}
#[cfg(feature = "dyn")]
struct MyStruct {
my_field: Box<dyn MyTrait>
}
Meu problema é que cada impl
bloco precisa ser duplicado por completo, quando as alterações deveriam afetar apenas algumas linhas.
#[cfg(not(feature = "dyn"))]
impl<T: MyTrait> MyStruct<T> {
fn new(field: T) -> Self {
Self { field }
}
}
#[cfg(feature = "dyn")]
impl MyStruct {
fn new(field: impl MyTrait + 'static) -> Self {
Self { field: Box::new(field) }
}
}
Existe uma maneira mais elegante de fazer isso? Macro black magic? Isso é apenas uma má ideia e não vale a pena fazer?
Editar
Por resposta de @cdhowie
Isso quase chega à compilação bem-sucedida! No entanto, MyTrait
na verdade tem um método (que eu retirei para obter um exemplo mínimo):
trait MyTrait: Sized {
fn calculate(&self, struct: &MyStruct<Self>);
}
a referência &MyStruct<Self>
é necessária porque esse método precisa acessar dados para cálculos.
Tentar implementar como sugerido não funciona... algo sobre isso quebra a compatibilidade dinâmica
impl MyTrait for Box<dyn MyTrait> {
// ^^^^^^^^^^^^^^^^
// ERROR
// the trait `MyTrait` cannot be made into an object
Se eu tentar implementar a característica para Box (não tenho certeza se isso resolveria meu problema), esse é o problema:
impl<T: MyTrait> MyTrait for Box<T> {
fn calculate(
&self,
struct: &MyStruct<Self>,
) {
(**self).calculate(struct)
// ^^^^^^
// ERROR: mismatched types
// expected reference &MyStruct<T>
// found reference &MyStruct<Box<T>>
}
}
Há pouca razão para realmente usar compilação condicional para fazer isso. Em vez disso, basta implementar
MyTrait
onBox<dyn MyTrait>
e então os consumidores podem usarMyStruct<Box<dyn MyTrait>>
se quiserem despacho dinâmico.Você também pode usar
T
como padrãoBox<dyn MyTrait>
, e então os usos básicos deMyStruct
usarão implicitamente o despacho dinâmico:Primeiro, não faça isso. Os recursos devem ser aditivos , porque os recursos são unificados. Imagine duas bibliotecas que dependem da sua biblioteca, uma assume
feature = "dyn"
e a outra não. Elas compilarão bem sozinhas, mas qualquer um que tente depender de ambas fará com que elas não compilem (e tenham muita dor de cabeça).Agora, supondo que você realmente não tenha uma variante da resposta do @cdhowie que funcione (um fato que não verifiquei completamente), a melhor solução é ter duas structs e duas traits. Claro, eles podem compartilhar código sempre que possível, mas a API deve ser diferente.