第5章 类型转换
说到底,一切都只是某处的一堆比特,而类型系统只是为了帮助我们正确使用这些比特。类型系统中有两个常见的问题:需要将这些确切的位重新解释为不同的类型,以及需要改变位以对不同的类型具有同等的意义。因为 Rust 鼓励在类型系统中对重要的属性进行编码,所以这些问题是非常普遍的。因此,Rust 给了你几种方法来解决它们。
首先,我们将看看 Safe Rust 给你提供的重新解释值的方法。最简单的方法是把一个值分解成它的组成部分,然后从它们中建立一个新的类型:
#![allow(unused)] fn main() { struct Foo { x: u32, y: u16, } struct Bar { a: u32, b: u16, } fn reinterpret(foo: Foo) -> Bar { let Foo { x, y } = foo; Bar { a: x, b: y } } }
但这最好也不过是一种烦人的做法。对于常见的转换,Rust 提供了更符合人体工程学的替代方法。
强转
在某些情况下,类型可以隐式地被强转。这些变化通常只是削弱类型,主要集中在指针和生命周期方面。它们的存在主要是为了让 Rust 在更多的情况下“正常工作”,而且基本上是无害的。
关于所有强转类型的详尽列表,请参见《The Reference》中的Coercion types部分。
请注意,在匹配 Trait 时,我们不进行强制转换(除了接收者,见下一页)。如果某个类型U有一个impl,而T可以强转到U,这并不构成T的实现。例如,下面的内容不会通过类型检查,尽管将t强转到&T是可以的,并且有针对&T的impl。
trait Trait {} fn foo<X: Trait>(t: X) {} impl<'a> Trait for &'a i32 {} fn main() { let t: &mut i32 = &mut 0; foo(t); }
这样编译失败:
error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
--> src/main.rs:9:9
|
3 | fn foo<X: Trait>(t: X) {}
| ----- required by this bound in `foo`
...
9 | foo(t);
| ^ the trait `Trait` is not implemented for `&mut i32`
|
= help: the following implementations were found:
<&'a i32 as Trait>
= note: `Trait` is implemented for `&i32`, but not for `&mut i32`
点运算符
点运算符将执行很多类型转换的魔法:它将执行自动引用、自动去引用和强制转换,直到类型匹配。方法查找的详细机制定义在这里,简要的概述如下:
假设我们有一个函数foo,它有一个接收器(一个self、&self或&mut self参数)。如果我们调用value.foo(),编译器需要确定Self是什么类型,然后才能调用该函数的正确实现。在这个例子中,我们将说value具有T类型。
我们将使用full-qualified syntax来更清楚地说明我们到底是在哪个类型上调用一个函数。
- 首先,编译器会检查是否可以直接调用
T::foo(value)。这被称为“按值”方法调用。 - 如果它不能调用这个函数(例如,如果这个函数的类型不对,或者一个 trait 没有为
Self实现),那么编译器就会尝试添加一个自动引用。这意味着编译器会尝试<&T>::foo(value)和<&mut T>::foo(value)。这被称为“autoref”方法调用。 - 如果这些候选方法都不奏效,它就对
T解引用并再次尝试。这使用了Deref特性——如果T: Deref<Target = U>,那么它就用U而不是T类型再试。如果它不能解除对T的引用,它也可以尝试 unsizingT。这只是意味着,如果T在编译时有一个已知的大小参数,那么在解析方法时它就会“忘记”它。例如,这个 unsizing 步骤可以通过“忘记”数组的大小将[i32; 2]转换成[i32]。
下面是一个方法查找算法的例子:
let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];
当数组在这么多的间接点后面时,编译器是如何实际计算array[0]的呢?首先,array[0]实际上只是Index特性的语法糖——编译器会将array[0]转换成array.index(0)。现在,编译器检查array是否实现了Index,这样它就可以调用这个函数。
然后,编译器检查Rc<Box<[T; 3]>>是否实现了Index,但它没有,&Rc<Box<[T; 3]>>和&mut Rc<Box<[T; 3]>>也没有。由于这些方法都不起作用,编译器将Rc<Box<[T; 3]>解引用到Box<[T; 3]>中,并再次尝试。Box<[T; 3]>、&Box<[T; 3]>和&mut Box<[T; 3]>没有实现Index,所以它再次解引用。[T; 3]和它的自动引用也没有实现Index。它不能再继续解引用[T; 3],所以编译器取消了它的大小,得到了[T]。最后,[T]实现了Index,所以它现在可以调用实际的index函数。
考虑一下下面这个更复杂的点运算符工作的例子:
#![allow(unused)] fn main() { fn do_stuff<T: Clone>(value: &T) { let cloned = value.clone(); } }
实现了Clone的是什么类型?首先,编译器检查是否可以按值调用。value的类型是&T,所以clone函数的签名是fn clone(&T) -> T。它知道T: Clone,所以编译器发现cloned: T。
如果取消T: Clone的限制,会发生什么?它将不能按值调用,因为T没有实现Clone。所以编译器会尝试通过自动搜索来调用。在这种情况下,该函数的签名是fn clone(&&T) -> &T,因为Self = &T。编译器看到&T: Clone,然后推断出cloned: &T。
下面是另一个例子,自动搜索行为被用来创造一些微妙的效果:
#![allow(unused)] fn main() { use std::sync::Arc; #[derive(Clone)] struct Container<T>(Arc<T>); fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) { let foo_cloned = foo.clone(); let bar_cloned = bar.clone(); } }
foo_cloned和bar_cloned是什么类型?我们知道,Container<i32>: Clone,所以编译器按值调用clone,得到foo_cloned: Container<i32>。然而,bar_cloned实际上有&Container<T>类型。这肯定是不合理的——我们给Container添加了#[derive(Clone)],所以它必须实现Clone! 仔细看看,由derive宏产生的代码是(大致):
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
派生的Clone实现是只在T: Clone的地方定义,所以没有Container<T>的实现。Clone在一般的T上没有实现。编译器接着查看&Container<T>是否实现了Clone,最终发现它实现了。因此,它推断出clone是由 autoref 调用的,所以bar_cloned的类型是&Container<T>。
我们可以通过手动实现Clone而不需要T: Clone来解决这个问题:
impl<T> Clone for Container<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
现在,类型检查器推断出,bar_cloned: Container<T>。
Casts
Casts(译者注:实在没有找到合适的中文表述)是强转的超集:每个强转都可以通过 cast 来明确调用。然而,有些转换需要 cast。虽然强转是普遍存在的,而且基本上是无害的,但是这些“真正的 cast”是罕见的,而且有潜在的危险。因此,必须使用as关键字来明确调用 cast:expr as Type。
你可以在《The Reference》中找到一个所有真正的 cast 和 cast 语义的详尽列表。
Casting 的安全性
真正的 cast 通常围绕着原始指针和原始数字类型。尽管它们很危险,但这些转换在运行时是不会出错的。如果一个 cast 触发了一些微妙的边界条件,也不会有任何迹象表明发生了这种情况,cast 会成功。也就是说,cast 必须在类型的级别上有效,否则会在编译时被静态地阻止。例如,7u8 as bool编译会出错。
也就是说, cast 并不是unsafe的,因为它们本身通常不会违反内存安全。例如,将一个整数转换为一个原始指针很容易导致可怕的事情,然而,创建指针的行为本身是安全的,因为实际使用一个原始指针已经被标记为unsafe。
一些关于 cast 的说明
cast raw slice 时的长度问题
请注意,在 cast raw slice 时,长度不会被调整:*const [u16] as *const [u8]创建的 slice 只包括原始内存的一半。
传递性
Casting 不是传递的,也就是说,即使e as U1 as U2是一个有效的表达式,e as U2也不一定是。
Transmutes
类型系统,别挡着我们的路!我们要重新解释这些比特,否则就会死掉!尽管这本书是关于做不安全的事情的,但我真的必须强调,你应该深入思考找到本节中所涉及的操作以外的另一种方法。这真的是你在 Rust 中所能做的最可怕的不安全的事情,而这基本不设防。
mem::transmute<T, U>接收一个T类型的值并将其重新解释为U类型。唯一的限制是T和U被验证为具有相同的大小。导致未定义行为的方法是令人难以置信的。
-
首先,创建一个具有无效状态的任何类型的实例都会导致无法真正预测的任意混乱。即使你从未对
bool做过任何事情,也不要把3转化为bool。就是不要。 -
Transmute 有一个重载的返回类型。如果你不指定返回类型,它可能会为了满足类型推导而返回一个令人惊讶的类型。
-
将一个
&转为&mut是未定义行为,尽管某些用法可能是安全的,但是需要注意,Rust 优化器可以自由地假设一个共享引用在它的生命周期内是不变的,而这种转换会违反这个假设。因此:- 将一个
&转为&mut总是未定义行为 - 不,你不能这样做
- 不,你并不特别
- 将一个
-
Transmute 到一个没有明确提供生命周期的引用会产生一个无限制的寿命
-
当在不同的复合类型之间转换时,你必须确保它们的布局是一样的!如果布局不同,错误的字段就会被填入错误的数据,这也许仅仅让你 Debug 一阵,也可能会造成 UB(见上文)
那么你怎么知道布局是否相同呢?对于
repr(C)类型和repr(transparent)类型,布局是精确定义的。但是对于普通的repr(Rust)来说,它不是。即使是同一个通用类型的不同实例也可以有截然不同的布局。Vec<i32>和Vec<u32>可能有相同的字段顺序,也可能没有。数据布局保证了什么,或者没保证什么的细节可以参考 UCG 工作组。
mem::transmute_copy<T, U>比这个更不安全。它把size_of<U>字节从T中复制出来,并把它们解释为U。mem::transmute的大小检查没有了(因为复制出一个前缀可能是有效的),尽管U比T大是未定义行为。
当然,你也可以使用原始指针转换或union来获得这些函数的所有功能,并关闭 Lint 或其他基本的合理性检查。原始指针转换和union并不能神奇地避免上述规则。