创建装箱特质的机制是如何工作的?

15

我很难理解装箱特质的值是如何产生的。考虑以下代码:

trait Fooer {
    fn foo(&self);
}

impl Fooer for i32 {
    fn foo(&self) { println!("Fooer on i32!"); }
}

fn main() {
    let a = Box::new(32);                        // works, creates a Box<i32>
    let b = Box::<i32>::new(32);                 // works, creates a Box<i32>
    let c = Box::<dyn Fooer>::new(32);           // doesn't work
    let d: Box<dyn Fooer> = Box::new(32);        // works, creates a Box<Fooer>
    let e: Box<dyn Fooer> = Box::<i32>::new(32); // works, creates a Box<Fooer>
}

显然,变量a和b可以轻松地工作。然而,变量c不行,可能是因为new函数只接受相同类型的值,而这种情况并不适用于Fooer != i32。变量d和e有效,这让我怀疑是否执行了某种自动转换,从Box到Box。
所以我的问题是:
  • 这里是否发生了某种转换?
  • 如果是,背后的机制是什么,它是如何工作的?(我也对底层细节感兴趣,即内部表示方式)
  • 是否有一种方法可以直接从i32创建Box?如果没有:为什么?

  • 我问自己,为什么你想要一个 Box<Fooer>。我认为你不太理解 Rust 中的 trait 是如何工作的。尝试在每个变量(a..e)上调用 .foo(),看看会发生什么。 - hellow
    2
    编译器会告诉你为什么 c 无法工作的确切原因:the method 'new' exists but the following trait bounds were not satisfied: 'Fooer : std::marker::Sized' - hellow
    这是关于方差的另一个问题。很抱歉,我无法提供帮助:这种东西仍然会触发我的情绪 :P - Boiethios
    1
    @hellow:关于错误信息:它似乎暗示着实际上无法调用 Box::<Fooer>::new,因为给定的类型参数必须在编译时具有已知大小,而这并不是 Fooer 的情况,对吧?所以我的假设是正确的,你必须先构造一个具有大小的类型的 Box,然后以某种方式将其转换为 trait 类型的 Box,这让我想到了我的最初问题:这种转换是通过什么机制进行的? - Askaga
    1
    据我所知,方差在Rust中仅限于生命周期,因为特征和实现结构之间没有子类型关系。没有“结构继承”。否则,鉴于Box<T>是变体的,我可以将Box<Box<i32>>强制转换为Box<Box<Fooer>>。另一方面,将Box<Box<&'a i32>>强制转换为Box<Box<&'b Fooer>>是可能的,其中'a:'b,因为'a:'b是一个实际的子类型关系,而不像i32:Fooer和不可变借用&'x T不仅是变体的,而且还涵盖了'x - Calculator
    显示剩余6条评论
    2个回答

    21
    然而,变量c没有,可能是因为new函数仅接受相同类型的值,这种情况并不适用于Fooer != i32。原因是Box<dyn Fooer>没有new函数可用。在文档中,大多数Box<T>方法允许T: ?Sized,但new定义在一个没有T: ?Sized约束的impl中。这意味着只有在已知大小的类型T时才能调用Box::<T>::new。由于dyn Fooer是无大小的,因此根本没有可调用的new函数。
    实际上,在当今的Rust中,这个函数是无法存在的。 Box<T>::new需要知道具体类型T,以便可以分配正确大小和对齐方式的内存。因此,在将其发送到Box::new之前,您无法抹去T
    (可以想象未来的语言扩展可能允许函数接受不定大小的参数;但是,即使unsized_locals也不清楚是否会真正启用Box<T>::new接受不定大小的T。)

    目前,像dyn Fooer这样的不定大小类型只能存在于“fat pointer”后面,即指向对象和指向该对象的Fooer实现的指针。如何获得fat pointer?您从thin pointer开始并进行强制转换。这就是这两行代码中发生的情况:

    let d: Box<Fooer> = Box::new(32);        // works, creates a Box<Fooer>
    let e: Box<Fooer> = Box::<i32>::new(32); // works, creates a Box<Fooer>
    

    Box::new返回一个Box<i32>,然后被强制转换Box<Fooer>。你可以考虑这是一种转换,但是Box并没有改变;编译器所做的只是在它上面加了一个额外的指针并忘记了它的原始类型。Rodrigo's answer 更详细地介绍了这种强制转换的语言级机制。

    希望所有这些都能解释为什么答案是否定的:

    有没有一种方法可以直接从i32创建Box<Fooer>

    答案是“没有”:必须先将i32装箱,然后才能抹掉其类型。这就是你不能写let x: Fooer = 10i32的原因。

    相关


    11
    我将尝试解释您的代码中发生的转换(强制转换)。

    有一个名为Unsize的标记特征,其中包括:

    T: Trait 时,TUnsize<Trait>

  • [...]
  • 据我所知,这个特征不直接用于强制转换。相反,使用CoerceUnsized。这个特征在许多情况下都被实现了,其中一些是相当预期的,例如:

    impl<'a, 'b, T, U> CoerceUnsized<&'a U> for &'b T 
    where
        'b: 'a,
        T: Unsize<U> + ?Sized,
        U: ?Sized
    

    这是用于将&i32强制转换为&Fooer的特质。
    实现该特质的有趣而不太明显的部分,会影响您的代码。
    impl<T, U> CoerceUnsized<Box<U>> for Box<T> 
    where
        T: Unsize<U> + ?Sized,
        U: ?Sized
    

    这个和Unsize标记的定义一起,可以理解为:如果U是一个特性(trait),T实现了U,那么Box<T>可以强制转换(coerced)成Box<U>
    关于你上一个问题:

    有没有办法直接从i32创建Box<Fooer>?如果没有:为什么?

    我不知道有没有。问题在于Box::new(T)需要一个大小已知的值,因为传递的值会被移动到盒子里,而不定长的值无法移动。
    在我看来,最简单的方法就是简单地写下:
    let c = Box::new(42) as Box<Fooer>;
    

    换句话说,您创建适当类型的Box,然后将其强制转换为未定大小的(请注意,它看起来与您的d示例非常相似)。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接