20240801
类型转换
本节学习会涉及简单的类型转换,也会涉及基于语言底层的运行过程得到的类型分析,最后是前沿但是不安全的Transmute介绍。
as转换
先来看一段代码:
fn main() {
let a: i32 = 10;
let b: u16 = 100;
if a < b {
println!("Ten is less than one hundred.");
}
}
很明显能看出该代码编译不通过,a和b拥有不同的类型,而对于Rust语言来说,不允许两种不同的类型进行比较。
解决办法很简单,只要把 b 转换成 i32 类型即可,Rust 中内置了一些基本类型之间的转换,这里使用 as 操作符来完成: if a < (b as i32) {…}。那么为什么不把 a 转换成 u16 类型呢?
因为每个类型能表达的数据范围不同,如果把范围较大的类型转换成较小的类型,会造成错误,因此我们需要把范围较小的类型转换成较大的类型,来避免这些问题的发生。
下面列出了常用的转换形式:
fn main() {
let a = 3.1 as i8;
let b = 100_i8 as i32; //这里100_i8表示b原本是i8类型的数据
let c = 'a' as u8; // 将字符'a'转换为整数,97
println!("{},{},{}",a,b,c)
}
内存地址转换为指针
fn main() {
let mut values: [i32; 2] = [1, 2];
let p1: *mut i32 = values.as_mut_ptr(); // 使用 values.as_mut_ptr() 获取数组的裸指针(*mut i32),并将其赋值给 p1。这个裸指针指向数组的第一个元素。
// 在Rust中,*mut i32 是一个裸指针类型,表示一个可变(mutable)的指向 i32 类型数据的指针。这里的 * 表示解引用操作符,mut 表示这个指针可以被用来修改它所指向的数据。
let first_address = p1 as usize; // 将p1内存地址转换为一个整数
let second_address = first_address + 4; // 4 == std::mem::size_of::<i32>(),i32类型占用4个字节,因此将内存地址 + 4
let p2 = second_address as *mut i32; // 将 second_address 转换回 *mut i32 类型的裸指针,并赋值给 p2。现在 p2 指向数组的第二个元素。
unsafe { //使用 unsafe 代码块来解引用 p2 并增加其值。因为裸指针的解引用是危险的,可能违反Rust的内存安全原则,所以需要在 unsafe 代码块中进行。
*p2 += 1;
}
assert_eq!(values[1], 3);
}
补充知识:转换不具有传递性 就算 e as U1 as U2 是合法的,也不能说明 e as U2 是合法的(e 不能直接转换成 U2)
TryInto 转换
在一些场景中,使用 as 关键字会有比较大的限制。如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用 TryInto :
fn main() {
let a: u8 = 10;
let b: u16 = 1500;
let b_: u8 = b.try_into().unwrap();
if a < b_ {
println!("Ten is less than one hundred.");
}
}
使用 b.try_into( ) 尝试将 b(u16 类型)转换为 u8 类型。try_into( ) 方法是一个类型转换方法,它会在转换成功时返回 Ok 包装的转换后的值,如果转换失败(例如,当源类型值超出目标类型的范围时),则返回 Err。使用 unwrap( ) 来解包 try_into( ) 的结果。如果转换成功,unwrap( ) 将返回转换后的值;如果转换失败,程序将 panic 并显示错误信息。在这个例子中,由于 1500 在 u8 的范围之外(u8 的最大值是 255),try_into( ) 将失败,但 unwrap( ) 强制解包结果,导致程序 panic。接着,代码中有一个条件语句 if a < b_,这个条件语句永远不会执行,因为 b.try_into( ) 会失败,而 unwrap( ) 会导致程序在之前就 panic 了。
正确的做法是处理 try_into( ) 可能返回的错误,而不是使用 unwrap( )。这里是一个修改后的示例,它使用 match 语句来优雅地处理错误:
fn main() {
let a: u8 = 10;
let b: u16 = 1500;
let b_: u8 = match b.try_into() {
Ok(value) => value,
Err(_) => {
eprintln!("Value out of range for u8");
return; // 这里会导致 main 函数退出,程序结束,即下面打印和比较的代码都不会被运行
}
};
println("b_ is {}",b_);
if a < b_ {
println!("Ten is less than one hundred.");
}
}
return
上述代码中用到return。在Rust中,return 关键字用于从当前的函数中退出并返回一个值。在 main 函数中使用 return 会导致程序立即退出并返回一个值给操作系统。由于 main 函数的返回类型是 (),即没有返回值,所以 return 后面不需要跟任何值。
上述代码如果不用return:
fn main() {
let a: u8 = 10;
let b: u16 = 1500;
let b_: u8 = match b.try_into() {
Ok(value) => value,
Err(_) => {
eprintln!("Value out of range for u8");
0
}
};
println("b_ is {}",b_);
if a < b_ {
println!("Ten is less than one hundred.");
}
}
运行结果:
Value out of range for u8
0
因为b_类型是u8,所以当match到Err(_)时,也要返回一个u32类型,为了符合整个程序的逻辑正确性,这里用一个小于等于10的数即可。不然就像刚才说的用return,只不过会直接退出当前main函数。
通用类型转换
虽然 as 和 TryInto 很强大,但是只能应用在数值类型上,可是 Rust 有如此多的类型,想要为这些类型实现转换,我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体:
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 的类型系统,但是它们的存在是为了让 Rust 在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。
首先,在匹配特征时,不会做任何强制转换(除了方法)。一个类型 T 可以强制转换为 U,不代表 impl T 可以强制转换为 impl U,例如下面的代码就无法通过编译检查:
trait Trait {}
fn foo<X: Trait>(t: X) {} //表示foo函数的参数是实现了Trait特征的对象,而泛型+特征约束的写法有诸多限制,下面说明
impl<'a> Trait for &'a i32 {}
fn main() {
let t: &mut i32 = &mut 0;
foo(t);
}
解释特征对象与泛型+特征约束写法的区别:比如现在有类型a和b都实现了Trait特征,当函数返回中用 impl Trait时,只能返回一个类型,返回a或者b,如果想要能够返回a和b,则需要使用特征对象来代替impl Trait的写法;又或者现在有一个动态数组Vec,数组中想要存放实现了Trait的对象,如果用泛型+特征约束的写法,Vec中只能存放a或者b的类型,而用特征对象的写法,比如Vec<Box<dyn Trait>>,这个数组内能够存放a和b的类型。
报错如下:
error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
--> src/main.rs:9:9
|
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`
&i32 实现了特征 Trait, &mut i32 可以转换为 &i32,但是 &mut i32 依然无法作为 Trait 来使用。
隐式强制转换
例子:
let a: f64 = 5.7;
let b: i32 = a; // 隐式转换,小数部分被丢弃
理想很丰满,现实很骨感,上述代码并不能正确编译,也就是不能隐式的转换,需更改为显示强制转换,如下:
let a: f64 = 5.7;
let b: i32 = a as i32; // 隐式转换,小数部分被丢弃
再看下一个例子:
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr; // 隐式转换
能够正确运行,&arr本应该是&[i32; 5]类型,现在隐式转换为&[i32]类型,表示数组可以隐式转换为切片。
在网络上也未找到更多关于Rust语言进行隐式转换的内容,故且先这样,继续往下学习。
学成归来,关于更多隐式类型转换和应用详见Deref解引用。
点操作符
方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。所以下面的讲解主要是Rust语言对点操作符的底层的实现,非常有必要去理解
假设有一个方法 foo,它有一个接收器(接收器就是 self、&self、&mut self 参数)。如果调用 value.foo(),编译器在调用 foo 之前,需要决定到底使用哪个 Self 类型来调用。现在假设 value 拥有类型 T。再进一步,我们使用完全限定语法来进行准确的函数调用:
1.首先,编译器检查它是否可以直接调用 T::foo(value),称之为值方法调用
2.如果上一步调用无法完成(例如方法类型错误或者特征没有针对 Self 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用: <&T>::foo(value) 和 <&mut T>::foo(value),称之为引用方法调用
3.若上面两个方法依然不工作,编译器会试着解引用 T ,然后再进行尝试。这里使用了 Deref 特征 —— 若 T: Deref<Target = U> (T 可以被解引用为 U),那么编译器会使用 U 类型进行尝试,称之为解引用方法调用
4.若 T 不能被解引用,且 T 是一个定长类型(在编译期类型长度是已知的),那么编译器也会尝试将 T 从定长类型转为不定长类型,例如将 [i32; 2] 转为 [i32]
5.若还是不行,最后编译器大喊一声:奇!
下面我们来用一个例子来解释上面的方法查找算法:
use std::rc::Rc;
fn main(){
// 假设T是i32类型
let array: Rc<Box<[i32; 3]>> = Rc::new(Box::new([1, 2, 3]));
let first_entry = array[0];
println!("The first entry is: {}", first_entry);
}
在 Rust 中,Rc 是一个提供引用计数的智能指针,用于在多个部分之间共享对数据的所有权。Box 是一个堆分配的智能指针,用于在单个部分拥有数据时管理内存。上述代码中array 是一个 Rc 智能指针,它指向一个 Box,而 Box 又包含一个类型为 [i32; 3] 的数组。这里可以不用理解Rc和Box,只用知道他们二者均没有 实现Index特征即可,Index特征也就是进行索引操作的能力。
代码中 let first_entry = array[0]; 这行试图通过索引访问 array 的第一个元素。然而,这里有一个问题:Rc 和 Box 都不能直接进行索引操作,因为它们不是数组或切片类型。那么编译器如何使用 array[0] 这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素?
1.首先, array[0] 只是Index特征的语法糖:编译器会将 array[0] 转换为 array.index(0) 调用,当然在调用之前,编译器会先检查 array 是否实现了 Index 特征。
2.接着,编译器检查 Rc<Box<[i32; 3]>> 是否有实现 Index 特征,结果是否,不仅如此,&Rc<Box<[i32; 3]>> 与 &mut Rc<Box<[i32; 3]>> 也没有实现。
3.上面的都不能工作,编译器开始对 Rc<Box<[i32; 3]>> 进行解引用,把它转变成 Box<[i32; 3]>
4.此时继续对 Box<[i32; 3]> 进行上面的操作 :Box<[i32; 3]>, &Box<[i32; 3]>,和 &mut Box<[i32; 3]> 都没有实现 Index 特征,所以编译器开始对 Box<[i32; 3]> 进行解引用,然后我们得到了 [i32; 3]
5.[i32; 3] 以及它的各种引用都没有实现 Index 索引,是不是很反直觉:D,在直觉中,数组都可以通过索引访问,实际上只有数组切片才可以通过索引访问!它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此 [i32; 3] 被转换成 [i32],也就是数组切片,它实现了 Index 特征,因此最终我们可以通过 index 方法访问到对应的元素。
再来看看以下更复杂的例子:
fn do_stuff<T: Clone>(value: &T) {
let cloned = value.clone();
}
上面例子中 cloned 的类型是什么?首先编译器检查能不能进行值方法调用, value 的类型是 &T,同时 clone 方法的签名也是 &T : fn clone(&T) -> T,因此可以进行值方法调用,再加上编译器知道了 T 实现了 Clone,因此 cloned 的类型是 T。
如果 T: Clone 的特征约束被移除呢?
fn do_stuff<T: Clone>(value: &T) {
let cloned = value.clone();
}
首先,从直觉上来说,该方法会报错,因为 T 没有实现 Clone 特征,但是真实情况是什么呢?
我们先来推导一番。 首先通过值方法调用就不再可行,因为 T 没有实现 Clone 特征,也就无法调用 T 的 clone 方法。接着编译器尝试引用方法调用,此时 T 变成 &T,在这种情况下, clone 方法的签名如下: fn clone(&&T) -> &T,接着我们现在对 value 进行了引用。 编译器发现 &T 实现了 Clone 类型,这里因为所有的引用类型都可以被复制,因为其实就是复制一份地址,因此可以推出 cloned 也是 &T 类型。
最终,我们复制出一份引用指针,这很合理,因为值类型 T 没有实现 Clone,只能去复制一个指针了。
下面的例子也是自动引用生效的地方:
use std::sync::Arc;
#[derive(Clone)]
struct Container<T>(Arc<T>);
fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
let foo_cloned: Container<i32> = foo.clone();
let bar_cloned: &Container<T> = bar.clone();
}
推断下上面的 foo_cloned 和 bar_cloned 是什么类型?提示: 关键在 Container 的泛型参数,一个是 i32 的具体类型,一个是泛型类型,其中 i32 实现了 Clone,但是 T 并没有。
首先要复习一下复杂类型派生 Clone 的规则:一个复杂类型能否派生 Clone,需要它内部的所有子类型都能进行 Clone。因此 Container(Arc) 是否实现 Clone 的关键在于 T 类型是否实现了 Clone 特征。
上面代码中,Container 实现了 Clone 特征,因此编译器可以直接进行值方法调用,此时相当于直接调用 foo.clone,其中 clone 的函数签名是 fn clone(&T) -> T,由此可以看出 foo_cloned 的类型是 Container。
然而,bar_cloned 的类型却是 &Container,这个不合理啊,明明我们为 Container 派生了 Clone 特征,因此它也应该是 Container 类型才对。万事皆有因,我们先来看下 derive 宏最终生成的代码大概是啥样的:
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
从上面代码可以看出,派生 Clone 能实现的根本是 T 实现了Clone特征:where T: Clone, 因此 Container 就没有实现 Clone 特征。
编译器接着会去尝试引用方法调用,此时 &Container 引用实现了 Clone,最终可以得出 bar_cloned 的类型是 &Container。
当然,也可以为 Container 手动实现 Clone 特征:
impl<T> Clone for Container<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
此时,编译器首次尝试值方法调用即可通过,因此 bar_cloned 的类型变成 Container。
Transmute变形
mem::transmute<T, U> 将类型 T 直接转成类型 U,唯一的要求就是,这两个类型占用同样大小的字节数。transmute 对于追求安全的Rust语言来说非常危险,充满了不安全行为,对于我这种初学者来说尽量避免使用,遇到的时候再仔细研究。下面列举两个有用的 transmute 应用场景。
将裸指针变成函数指针:
fn foo() -> i32 {
0
}
let pointer = foo as *const ();
let function = unsafe {
// 将裸指针转换为函数指针
std::mem::transmute::<*const (), fn() -> i32>(pointer)
};
assert_eq!(function(), 0);
延长生命周期,或者缩短一个静态生命周期寿命:
struct R<'a>(&'a i32);
// 将 'b 生命周期延长至 'static 生命周期
unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
std::mem::transmute::<R<'b>, R<'static>>(r)
}
// 将 'static 生命周期缩短至 'c 生命周期
unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>) -> &'b mut R<'c> {
std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
}
以上例子非常先进!但是是非常不安全的 Rust 行为!