类型系统
Rust 是强类型语言,同时也是强安全语言,这些特性导致了 Rust 的类型注定比一般语言要更深入也更困难。
类型转换
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;
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();
let first_addr = p1 as usize; // 将p1内存地址转换为整数
let second_addr = first_addr + 4; // i32 占用4个字节
let p2 = second_addr as *mut i32;
unsafe {
*p2 += 4;
}
println!("{:?}", values); // [1, 6]
}
TryInto
转换
在一些场景中,使用 as
关键字会有比较大的限制。如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用TryInto
:
use std::convert::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.");
}
}
上面代码中引入了 std::convert::TryInto
特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中,我们在上面用到了 try_into
方法,因此需要引入对应的特征。但是 Rust 又提供了一个非常便利的办法,把最常用的标准库中的特征通过std::prelude
模块提前引入到当前作用域中,其中包括了 std::convert::TryInto
,你可以尝试删除第一行的代码 use ...
,看看是否会报错。
try_into
会尝试进行一次转换,并返回一个 Result
,此时就可以对其进行相应的错误处理。由于我们的例子只是为了快速测试,因此使用了 unwrap
方法,该方法在发现错误时,会直接调用 panic
导致程序的崩溃退出,在实际项目中,请不要这么使用。
最主要的是 try_into
转换会捕获大类型向小类型转换时导致的溢出错误:
fn main() {
let b: i16 = 1500;
let b_: u8 = match b.try_into() {
Ok(b1) => b1,
Err(e) => {
println!("{:?}", e.to_string());
0
}
};
}
运行后输出如下 "out of range integral type conversion attempted",在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把 1500_i16
转换为 u8
类型,后者明显不足以承载这么大的值。
关键要点
as
关键字处理数值转换TryInto
处理转换,如果出现出现错误,则会进行错误处理
通用类型转换
虽然 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 在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。
首先,在匹配特征时,不会做任何强制转换(除了方法)。一个类型 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);
}
报错如下:
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`
newtype
何为 newtype
?简单来说,就是使用元组结构体的方式将已有的类型包裹起来:struct Meters(u32);
,那么此处 Meters
就是一个 newtype
。
为何需要 newtype
?Rust 这多如繁星的 Old 类型满足不了我们吗?这是因为:
- 自定义类型可以让我们给出更有意义和可读性的类型名,例如与其使用
u32
作为距离的单位类型,我们可以使用Meters
,它的可读性要好得多 - 对于某些场景,只有
newtype
可以很好地解决 - 隐藏内部类型的细节
为外部类型实现外部特征
如果在外部类型上实现外部特征必须使用 newtype
的方式,否则你就得遵循孤儿规则:要为类型 A
实现特征 T
,那么 A
或者 T
必须至少有一个在当前的作用范围内。
例如,如果想使用 println!("{}", v)
的方式去格式化输出一个动态数组 Vec
,以期给用户提供更加清晰可读的内容,那么就需要为 Vec
实现 Display
特征,但是这里有一个问题: Vec
类型定义在标准库中,Display
亦然,这时就可以祭出大杀器 newtype
来解决:
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
// 为newtype是Wrapper实现Display特征
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
如上所示,使用元组结构体语法 struct Wrapper(Vec<String>)
创建了一个 newtype
Wrapper,然后为它实现 Display
特征,最终实现了对 Vec
动态数组的格式化输出。
更好的可读性及类型异化
use std::ops::Add;
use std::fmt;
struct Meters(u32);
impl fmt::Display for Meters {
// 为newtype是Meters实现Display特征
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); // 目标距离你30米
}
fn calculate_distance(d1: Meters, d2: Meters) -> Meters {
d1 + d2
}
上面代码创建了一个 newtype
Meters
,为其实现 Display
和 Add
特征,接着对两个距离进行求和计算,最终打印出该距离:
目标地点距离你30米
事实上,除了可读性外,还有一个极大的优点:如果给 calculate_distance
传一个其它的类型,例如 struct MilliMeters(u32);
,该代码将无法编译。尽管 Meters
和 MilliMeters
都是对 u32
类型的简单包装,但是它们是不同的类型!
隐藏内部类型的细节
众所周知,Rust 的类型有很多自定义的方法,假如我们把某个类型传给了用户,但是又不想用户调用这些方法,就可以使用 newtype
:
struct Meters(u32);
fn main() {
let i: u32 = 2;
assert_eq!(i.pow(2), 4);
let n = Meters(i);
// 下面的代码将报错,因为`Meters`类型上没有`pow`方法
// assert_eq!(n.pow(2), 4);
}