web-dev-qa-db-fra.com

Comment obtenir une référence à un type concret à partir d'un objet trait?

Comment puis-je obtenir Box<B> ou &B ou &Box<B> à partir de la variable a dans ce code:

trait A {}

struct B;
impl A for B {}

fn main() {
    let mut a: Box<dyn A> = Box::new(B);
    let b = a as Box<B>;
}

Ce code renvoie une erreur:

error[E0605]: non-primitive cast: `std::boxed::Box<dyn A>` as `std::boxed::Box<B>`
 --> src/main.rs:8:13
  |
8 |     let b = a as Box<B>;
  |             ^^^^^^^^^^^
  |
  = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait
33
Aleksandr

Il y a deux façons de faire du downcasting à Rust. La première consiste à utiliser Any . Notez que ceci seulement vous permet de rabattre vers le type de béton exact et original. Ainsi:

use std::any::Any;

trait A {
    fn as_any(&self) -> &dyn Any;
}

struct B;

impl A for B {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let a: Box<dyn A> = Box::new(B);
    // The indirection through `as_any` is because using `downcast_ref`
    // on `Box<A>` *directly* only lets us downcast back to `&A` again.
    // The method ensures we get an `Any` vtable that lets us downcast
    // back to the original, concrete type.
    let b: &B = match a.as_any().downcast_ref::<B>() {
        Some(b) => b,
        None => panic!("&a isn't a B!"),
    };
}

L'autre façon consiste à implémenter une méthode pour chaque "cible" sur le trait de base (dans ce cas, A), et à implémenter les transtypages pour chaque type de cible souhaité.


Attendez, pourquoi avons-nous besoin de as_any?

Même si vous ajoutez Any comme exigence pour A, cela ne fonctionnera toujours pas correctement. Le premier problème est que le A dans Box<dyn A> sera aussi implémentera Any... ce qui signifie que lorsque vous appelez downcast_ref, vous l'appellerez en fait sur le type d'objet A. Any can seulement downcast vers le type sur lequel elle a été invoquée, qui dans ce cas est A, vous ne pourrez donc effectuer qu'une conversion &dyn A que vous aviez déjà.

Mais il y a une implémentation de Any pour le type sous-jacent là quelque part, non? Eh bien, oui, mais vous ne pouvez pas y arriver. Rust ne vous permet pas de "cross cast" de &dyn A à &dyn Any.

Ça est ce que as_any est pour; parce que c'est quelque chose d'implémenté uniquement sur nos types "concrets", le compilateur ne se trompe pas quant à celui qu'il est censé invoquer. L'appeler sur un &dyn A provoque sa répartition dynamique vers l'implémentation concrète (encore une fois, dans ce cas, B::as_any), qui renvoie un &dyn Any en utilisant l'implémentation de Any pour B, c'est ce que nous voulons.

Notez que vous pouvez contourner tout ce problème en n'utilisant tout simplement pas A du tout. Plus précisément, ce qui suit aussi fonctionnera:

fn main() {
    let a: Box<dyn Any> = Box::new(B);
    let _: &B = match a.downcast_ref::<B>() {
        Some(b) => b,
        None => panic!("&a isn't a B!")
    };    
}

Cependant, cela vous empêche d'avoir des méthodes autres; tous vous pouvez faire ici est abattu à un type concret.

Comme dernière note d'intérêt potentiel, la caisse mopa vous permet de combiner la fonctionnalité de Any avec un trait qui vous est propre.

48
DK.

Il doit être clair que le cast peut échouer s'il existe un autre type C implémentant A et que vous essayez de cast Box<C> dans une Box<B>. Je ne connais pas votre situation, mais pour moi, cela ressemble beaucoup à ce que vous introduisez des techniques d'autres langages, comme Java, dans Rust. Je n'ai jamais rencontré ce genre de problème en Rust - peut-être que la conception de votre code pourrait être améliorée pour éviter ce genre de distribution.

Si vous le souhaitez, vous pouvez "lancer" à peu près n'importe quoi avec mem::transmute . Malheureusement, nous aurons un problème si nous voulons simplement lancer Box<A> à Box<B> ou &A à &B car un pointeur vers un trait est un gros pointeur qui se compose en fait de deux pointeurs: un vers l'objet réel, un vers le vptr. Si nous le convertissons en un type struct, nous pouvons simplement ignorer le vptr. N'oubliez pas que cette solution est très dangereuse et assez hacky - je ne l'utiliserais pas en "vrai" code.

let (b, vptr): (Box<B>, *const ()) = unsafe { std::mem::transmute(a) };

EDIT: Vissez ça, c'est encore plus dangereux que je ne le pensais. Si vous voulez le faire correctement de cette façon, vous devez utiliser std::raw::TraitObject . C'est encore instable cependant. Je ne pense pas que cela soit d'une quelconque utilité pour OP; ne l'utilisez pas!

Il existe de meilleures alternatives dans cette question très similaire: Comment faire correspondre les implémenteurs de traits

4
Lukas Kalbertodt