Eu encontrei um comportamento de variância estranho que tenho certeza de que é uma falha na minha compreensão do sistema de tipos, mas parece um bug do compilador.
trait Trait: 'static {}
impl<T> Trait for T where T: 'static {}
struct Inner<T>(T);
fn make_inner<T: Trait>(t: T) -> Inner<Box<T>> {
Inner(Box::new(t))
}
type DynNecessary = Vec<Inner<Box<dyn Trait>>>;
struct DynCarrier {
things: DynNecessary,
}
impl DynCarrier {
pub fn spawn(&mut self, t: impl Trait + 'static) {
let inner = make_inner(t);
self.things.push(inner);
}
}
fn main() {}
No exemplo acima, make_inner
recebe T
e retorna Inner<Box<T>>
onde T é conhecido por satisfazer Trait
.
No entanto, ao tentar usar o valor de retorno de make_inner
algum lugar que aceita Inner<Box<dyn Trait>>
, obtenho o seguinte:
= note: expected struct `Inner<Box<(dyn Trait + 'static)>>`
found struct `Inner<Box<impl Trait + 'static>>`
Eu tinha um palpite de que isso tinha algo a ver com a maneira como o programa em tempo de execução trataria um dyn Trait
versus um concreto impl Trait
, mas onde isso desmorona para mim é na seguinte mudança:
fn make_inner<T: Trait>(t: T) -> Inner<Box<dyn Trait>> {
Inner(Box::new(t))
}
onde now t
é coagido com sucesso para dyn Trait
.
Que influência a inferência de tipo de retorno tem sobre tipos, que a inferência de tipo de argumento não tem? Se .push
assume a propriedade de inner
, por que não é capaz do mesmo tipo de coerção?
A coisa a lembrar aqui é que
dyn Trait
é um tipo concreto . É um tipo concreto estranho , que por acaso compartilha sua representação na memória com muitos outros tipos, mas é um tipo.Box<dyn Trait>
, portanto, também é um tipo concreto específico (com seu próprio layout particular na memória que inclui um ponteiro vtable).Na primeira versão do código, você está construindo um
Inner<Box<T>>
, ondeT
há algum tipo implementandoTrait
(andT ≠ dyn Trait
), e então tentando usá-lo ondeInner<Box<dyn Trait>>
é esperado. Isso falha porque é uma incompatibilidade de tipos.Mas e quanto à coerção para
dyn Trait
? Por que isso não está acontecendo? Porque isso só acontece para tipos específicos de contenção . Especificamente, nas versões atuais do Rust (a maneira como é descrito pode mudar em versões futuras), você pode consultar oCoerceUnsized
trait unstable para ver todos os tipos que podem ser coagidos dessa maneira. Note queBox
está nesta lista — então você pode coagirBox<T>
paraBox<dyn Trait>
— mas, claro, seuInner
não.Na segunda versão com
-> Inner<Box<dyn Trait>>
, o tipo de retorno declarado informa a inferência de tipo dentromake_inner()
de tal forma que a construção deInner
é entendida como requerendo um parâmetro do tipoBox<dyn Trait>
, e que a coerção é suportada. O princípio geral aqui é: aBox<T>
deve ser coagido para aBox<dyn Trait>
antes de você envolvê-lo em (quase) qualquer outra coisa . Definir o tipo de retorno é uma maneira de fazer isso acontecer.Uma demonstração, ou possivelmente uma solução alternativa útil, é que seu código original será compilado se você desmontá-lo e recriá-lo
Inner
com o tipo correto:Ou, se você estiver satisfeito em usar recursos instáveis, basta implementar
CoerceUnsized
e seu código funcionará sem outras alterações:Quando isso
impl
for usado,T
será igual aoBox<impl Trait>
tipo eU
será igual aBox<dyn Trait>
.No entanto, eu recomendaria a solução simples que você já encontrou: especifique
Box<dyn Trait>
com antecedência suficiente que a coerção aconteça no momento da construção.