LOADING

加载过慢请开启缓存 浏览器默认开启

Rust 系统编程(三):动态类型强制转换 - CoerceUnsized

Rust 语言提供了前所未有的保护机制,其类型检查严格堪称举世无双,开发人员在使用 Rust 语言的时候能够得到优雅的可读性,但是也受到 Rust 高度抽象的烦恼。Rust 系统编程系列文章旨在深入探索 Rust 的特性,从 Rust 本身的设计哲学的视角中去理解 Rust 的细节。

本文是动态分发深入理解的正式的第一节,探讨了 Rust 是如何利用 trait 机制将对象强制转换为动态类型的操作规范化的。

Rust 系统编程系列:

如果不想深入探讨 Rust 的设计美学,可以跳转到实例代码快速理解:


trait CoerceUnsized,从名字上来看,意思是强制转换成不定大小类型。这个特质指示了一个指针或是一个封装,它让被指向对象可以从确定大小的类型变为不确定大小的类型,这是什么意思呢?举个例子,一个静态数组类型 [u8;2] 显然就是两个 u8 数据连续排列,但当我们尝试将它传入其他地方使用的时候,我们通常会传入这个数组的基地址,这个时候这个数组就会变成一种动态大小的类型了 [u8],而这个数组的长度是动态确定的(即不在编译期间确定为一个常数),在 Rust 中实际上被解释为一个胖指针,这个胖指针的元数据表示了数组长度。你会发现,这种转换在实际应用中很合理,但是面对类型检查如此严格的 Rust 里却有些奇怪,毕竟这是不一样的类型了。在文档中,他建议我们可以去查看 DST coercion RFCthe nomicon entry on coercion 以深入了解其中的细节。

对于内建指针类型(即原来就有的,非自定义的),如果 T: Unsize<U>,那么指向 T 的指针可以通过从瘦指针转换为胖指针来隐式转换为指向 U 的指针。在这里,Unsize<U> 也是一个 trait,它表示可以转换为一个动态大小类型 U(Dynamic Size Type, DST)。此时,从瘦指针转换为胖指针的元数据是由 U 决定的(当然也可能和 T 相关,例如上面提到的 [u8;2])。

对于自定义类型,这种强制转换通过 Foo<T>Foo<U> 转换来实现,当然前提是 CoerceUnsized<Foo<U>> for Foo<T> 的实现是存在的。这里提到了一个关键信息,我们可以通过实现 CoerceUnsized 来让我们的泛型类型做到隐式转换,但前提是内部的泛型数据本身是可以隐式转换的,这隐含了另一条信息,就是前面说到的内建指针类型是已经实现好了的。果不其然,在后面的代码中就为内建类型实现了这个 CoerceUnsized

// &mut T -> &mut U
impl<'a, T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<&'a mut U> for &'a mut T {}
// &mut T -> &U
impl<'a, 'b: 'a, T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<&'a U> for &'b mut T {}
// &mut T -> *mut U
impl<'a, T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<*mut U> for &'a mut T {}
// &mut T -> *const U
impl<'a, T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<*const U> for &'a mut T {}

// &T -> &U
impl<'a, 'b: 'a, T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<&'a U> for &'b T {}
// &T -> *const U
impl<'a, T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<*const U> for &'a T {}

// *mut T -> *mut U
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<*mut U> for *mut T {}
// *mut T -> *const U
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<*const U> for *mut T {}

// *const T -> *const U
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<*const U> for *const T {}

还需注意,文档中说明了对于一个自定义类型,CoerceUnsized 只能用于只涉及一个泛型的结构体,例如 Foo<T>,并且在其中包含 T 的域只能有一个(PhantomData<T> 除外),否则,你会收到下面的错误:

  • implementing the trait CoerceUnsized requires multiple coercions
  • CoerceUnsized may only be implemented for a coercion between structures with one field being coerced

假如,其中的 T 也是另一个自定义类型 Bar<T>,那么为了让 CoerceUnsizedFoo<T> 生效,CoerceUnsized<Bar<U>> for Bar<T> 必须存在,这个时候,隐式转换会将 Bar<T> 转换为 Bar<U>,剩下的域不包含 T,因而直接从 Foo<T> 拷贝至 Foo<U>

一般地,对于智能指针而言,你可能会实现一个

impl<T, U> CoerceUnsized<Ptr<U>> for Ptr<T> where T: Unsize<U>, U: ?Sized {}

这其中,T: ?Sized 是可选的,正如前面的 [u8;2] 转换为 [u8] 一样是从 Sized 转换为 Unsize<[u8] 的。或者严格来说,是 Box<[u8;2]> 转换为 Box<[u8]

对于直接嵌入 T 的封装类型,如 Cell<T>RefCell<T>,你可以直接实现一个

impl<T, U> CoerceUnsized<Wrap<U>> for Wrap<T> where T: CoerceUnsized<U> {}

这样,就可以让 Cell<Box<T>> 这样的多层复合类型也能作隐式转换。

这样一来,我们可以看到,其实这个 CoerceUnsized trait 给了我们一种自定义隐式转换的方式。

前面说的很复杂,为了便于理解,或者为了快速理解,下面给出实际的例子说明这个 CoerceUnsized 的用法。

例(1) 从静态数组到数组切片

这个例子考虑将一个泛型参数为静态数组类型的结构体转换为泛型参数为数组切片类型的结构体。

#![allow(unused)]
#![feature(unsize)]
#![feature(coerce_unsized)]

use core::{marker::Unsize, ops::CoerceUnsized};

struct MyWrap<T: ?Sized> {
    ptr: *const T,
}

// CoerceUnsized 告诉编译器 MyWrap 这个结构体可以进行强制类型转换
impl<T, U> CoerceUnsized<MyWrap<U>> for MyWrap<T>
where
    T: Unsize<U>,
    U: ?Sized,
{
}

fn main() {
    let c_vec = [0_i32; 2];
    let c = MyWrap {
        ptr: &c_vec as *const [i32; 2],
    };
    let cc = c as MyWrap<[i32]>; // 如果不实现 CoerceUnsized,这句代码就会报错
}
例(2) 从具体类型到特质对象

这个例子考虑将一个泛型参数为具体的自定义类型的结构体转换为一个泛型参数为 trait 对象的结构体。MyWrap 定义复用例2.1,这里只是给出用法。

struct Sa {
    data: i32,
}

trait Ta {}

impl Ta for Sa {}

fn main() {
    let d_data = Sa { data: 100 };
    let d = MyWrap {
        ptr: &d_data as *const Sa,
    };
    let dd = d as MyWrap<dyn Ta>; // 如果不实现 CoerceUnsized,这里的强制转换会报错
}

结语

使用一句话来概括一下:CoerceUnsized 提供一种标记,它告诉编译器将某一个静态定义的对象强制转换为动态大小的对象(DST)是合法的。