类型转换
as 转换
先来看一段代码:
fn main() {
let a: i32 = 10;
let b: u16 = 100;
if a < b {
println!("Ten is less than one hundred.");
}
}
rust
这段代码很明显会报错,因为 a 和 b 拥有不同的类型,Rust 不允许两种不同的类型进行比较。
解决办法很简单,只要把 b 转换成 i32 类型即可,Rust 中内置了一些基本类型之间的转换,这里使用 as 操作符来完成: if a < (b as i32) {...}。
从较小的类型转换成较大的类型没有任何问题,但是从较大的类型转换成较小的类型就需要注意了,转换时可能会造成数据丢失:
fn main() {
let val:u32 = 0b11111111_11111111_11111111_11111111;
println!("binary = {:#b}, value = {0}", val);
let val:u8 = val as u8;
println!("binary = {:#b}, value = {0}", val);
}
rust
输出:
binary = 0b11111111111111111111111111111111, value = 4294967295 binary = 0b11111111, value = 255bash
下面是常用的转换形式:
fn main() {
let a = 3.1 as i8;
let b = 100_i8 as i32;
let c = 'a' as u8; // 将字符'a'转换为整数,97
println!("{},{},{}",a,b,c)
}
rust
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.");
}
}
rust
std::convert::TryInto特征已经被std::prelude提前引入,因此不需要手动导入。
这里的代码实际上最终会 panic 掉,因为 1500 对于 u8 来说太大了,转换会导致溢出,因此直接 unwrap 会报错。
通用类型转换
首先,在匹配特征时,不会做任何强制转换(除了方法)。一个类型 T 可以强制转换为 U,不代表 impl T 可以强制转换为 impl U,例如下面的代码就无法通过编译检查:
trait Trait {}
fn foo<X: Trait>(t: X) {}
impl<'a> Trait for &'a i32 {}
fn main() {
let t: &mut i32 = &mut 0;
foo(t);
}
rust
报错如下:
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`
bash
&i32 实现了特征 Trait, &mut i32 可以转换为 &i32,但是 &mut i32 依然无法作为 Trait 来使用。
点操作符
方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。
假设有一个方法 foo,它有一个接收器(接收器就是 self、&self、&mut self 参数)。如果调用 value.foo(),编译器在调用 foo 之前,需要决定到底使用哪个 Self 类型来调用。现在假设 value 拥有类型 T。
再进一步,我们使用完全限定语法来进行准确的函数调用:
- 首先,编译器检查它是否可以直接调用
T::foo(value),称之为值方法调用 - 如果上一步调用无法完成(例如方法类型错误或者特征没有针对
Self进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用:<&T>::foo(value)和<&mut T>::foo(value),称之为引用方法调用 - 若上面两个方法依然不工作,编译器会试着解引用
T,然后再进行尝试。这里使用了Deref特征 —— 若T: Deref<Target = U>(T可以被解引用为U),那么编译器会使用U类型进行尝试,称之为解引用方法调用 - 若
T不能被解引用,且T是一个定长类型(在编译期类型长度是已知的),那么编译器也会尝试将T从定长类型转为不定长类型,例如将[i32; 2]转为[i32] - 若还是不行,编译失败。
例如下面的代码:
let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];
rust
- 首先,
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]>,&Box<[T; 3]>,和&mut Box<[T; 3]>都没有实现Index特征,所以编译器开始对Box<[T; 3]>进行解引用,然后我们得到了[T; 3] [T; 3]以及它的各种引用都没有实现Index索引(是不是很反直觉:D,在直觉中,数组都可以通过索引访问,实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此[T; 3]被转换成[T],也就是数组切片,它实现了Index特征,因此最终我们可以通过index方法访问到对应的元素。
newtype 和 类型别名
newtype
newtype 简单来说就是使用元组结构体的方式将已有的类型包裹起来,例如:struct Meters(u32),此处 Meters 就是一个 newtype。
为外部类型实现外部特征
由于孤儿规则的存在,要为类型 A 实现特征 T,那么 A 或者 T 必须至少有一个在当前的作用范围内。
例如,如果想使用 println!("{}", v) 的方式去格式化输出一个动态数组 Vec,以期给用户提供更加清晰可读的内容,那么就需要为 Vec 实现 Display 特征,但是这里有一个问题: Vec 类型定义在标准库中,Display 亦然,这时就可以祭出大杀器 newtype 来解决:
use std::fmt;
struct Wrapper<'a>(&'a Vec<String>);
impl<'a> fmt::Display for Wrapper<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let vec = vec![String::from("hello"), String::from("world")];
let w = Wrapper(&vec);
println!("w = {}", w);
}
rust
更好的可读性以及类型异化
更好的可读性不等于更少的代码,其次下面的例子只是一个示例,未必能体现出更好的可读性:
use std::ops::Add;
use std::fmt;
struct Meters(u32);
impl fmt::Display for Meters {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "目标地点距离你{}米", self.0)
}
}
impl Add for Meters {
type Output = Self;
fn add(self, other: Meters) -> Self {
Self(self.0 + other.0)
}
}
fn main() {
let d = calculate_distance(Meters(10), Meters(20));
println!("{}", d);
}
fn calculate_distance(d1: Meters, d2: Meters) -> Meters {
d1 + d2
}
rust
上面代码创建了一个 newtype Meters,为其实现 Display 和 Add 特征,接着对两个距离进行求和计算,最终打印出该距离:
目标地点距离你30米shell
事实上,除了可读性外,还有一个极大的优点:如果给 calculate_distance 传一个其它的类型,例如 struct MilliMeters(u32) 甚至 u32,该代码将无法编译。尽管都使用了 u32 类型,但是它们是不同的类型!
类型别名
除了使用 newtype,还可以使用一个更传统的方式来创建新类型:类型别名
type Meters = u32
rust
类型别名并不是一个独立的全新的类型,而是某一个类型的别名:
type Meters = u32;
let x: u32 = 5;
let y: Meters = 5;
println!("x + y = {}", x + y);
rust
上面的代码将顺利编译通过。
类型别名除了让类型可读性更好,还能减少模版代码的使用:
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
}
rust
由于这些类型标注太多,并且可以复用,所以可以直接使用类型别名替换:
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
}
rust
Sized 和不定长类型 DST
在 Rust 中类型有多种抽象的分类方式,例如:基本类型、集合类型、复合类型等。再比如说,如果从编译器何时能获知类型大小的角度出发,可以分成两类:
- 定长类型( sized ),这些类型的大小在编译时是已知的
- 不定长类型( unsized ),与定长类型相反,它的大小只有到了程序运行时才能动态获知,这种类型又被称之为 DST
动态大小类型 DST
对于动态大小类型编译器无法在编译期得知该类型值的大小,只有到了程序运行时,才能动态获知。对于动态类型,用 DST(dynamically sized types)或者 unsized 类型来称呼它。
试图创建动态大小的数组:
fn my_function(n: usize) {
let array = [123; n];
}
rust
以上代码就会报错(错误输出的内容并不是因为 DST,但根本原因是类似的),因为 n 在编译期无法得知,而数组类型的一个组成部分就是长度,长度变为动态的,自然类型就变成了 unsized 。
Rust 中常见的 DST 类型有: str、[T]、dyn Trait,它们都无法单独被使用,必须要通过引用或者 Box 来间接使用。
Sized 特征
编译器会自动帮我们在泛型约束上添加 Sized 特征约束,例如下面的函数没有对泛型 T 作任何限制:
fn generic<T>(t: T) {
// --snip--
}
rust
但是编译器会自动添加 Sized 特征:
fn generic<T: Sized>(t: T) {
// --snip--
}
rust
所有在编译时就能知道其大小的类型,都会自动实现 Sized 特征,除了 str 和特征。
每一个特征都是一个可以通过名称来引用的动态大小类型。因此如果想把特征作为具体的类型来传递给函数,你必须将其转换成一个特征对象:诸如 &dyn Trait 或者 Box<dyn Trait> (还有 Rc<dyn Trait>)这些引用类型。
如果非要使用没有 Sized 特征的动态类型数据,可以使用 ?Sized 特征:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
rust
?Sized 特征用于表明类型 T 既有可能是固定大小的类型,也可能是动态大小的类型。还有一点要注意的是,函数参数类型从 T 变成了 &T,因为 T 可能是动态大小的,因此需要用一个固定大小的指针(引用)来包裹它。
枚举和整数的转换
在 Rust 中以下代码:
enum MyEnum {
A = 1,
B,
C,
}
fn main() {
// 将枚举转换成整数,顺利通过
let x = MyEnum::C as i32;
// 将整数转换为枚举,失败
match x {
MyEnum::A => {}
MyEnum::B => {}
MyEnum::C => {}
_ => {}
}
}
rust
就会报错: MyEnum::A => {} mismatched types, expected i32, found enum MyEnum。
最简单,但也是最不安全的方式是使用 std::mem::transmute 实现:
// 控制底层类型的大小
#[repr(i32)]
enum MyEnum {
A = 1, B, C
}
fn main() {
let x = MyEnum::C;
let y = x as i32;
let z: MyEnum = unsafe { std::mem::transmute(2_i32) };
// match the enum that came from an int
match z {
MyEnum::A => { println!("Found A"); }
MyEnum::B => { println!("Found B"); }
MyEnum::C => { println!("Found C"); }
}
}
rust