一文快速理解Rust语言扩展trait

托尼托尼·乔巴 和 凯撒·库朗

科学无非就是在自然界的多样性中寻求统一性(或者更确切地说,是在我们经验的多样性中寻求统一性)。用 Coleridge 的话说,诗歌、绘画、艺术,同样是在多样性中寻求统一性

——Jacob Bronowski

Rust “实用工具” trait,这是标准库中各种 trait 的“百宝箱”,它们对 Rust 的编写方式有相当大的影响,所以,只有熟悉它们,你才能写出符合 Rust 语言惯例的代码并据此为你的 crate 设计公共接口,让用户认为这些接口是符合 Rust 风格的

语言扩展trait

运算符重载trait能让你在自己的类型上使用 Rust 的表达式运算符,同样,还有其他几个标准库 trait 也是 Rust 的扩展点,允许你把自己的类型更紧密地集成进语言中。这类trait包括 DropDeref 和 DerefMut,以及转换trait From 和 Into

语言扩展trait汇总表

trait 描述
Drop 析构器。每当丢弃一个值时,Rust 都要自动运行的清理代码
Deref 与 DerefMut 智能指针类型的trait
From 与 Into 用于将一种类型的值转换为另一种类型的转换trait
TryFrom 与 TryInto 用于将一种类型的值转换为另一种类型的转换trait,用于可能失败的转换

Drop

Drop 是标准库内置的,也是一个特殊的 trait,它定义了一个叫做 drop 的方法。这个方法在值离开作用域时被自动调用。这个特性可以用来执行一些清理工作,比如释放资源

1
2
3
4
5
6
7
8
9
10
11
12
struct MyType;

impl Drop for MyType {
fn drop(&mut self) {
println!("Dropping MyType");
}
}

fn main() {
let my_type = MyType;
// my_type 离开作用域,drop方法被自动调用
}

在这个例子中,我们定义了一个 MyType 类型,并为它实现了 Drop trait。当 my_instance 离开作用域时,drop 方法会被自动调用,打印出 “Dropping MyType”

注意!

Rust的Drop trait是在值离开作用域时被自动调用的,而不是在值被销毁时。这意味着,如果一个值被移动到另一个作用域,它的drop方法不会被调用

当一个值的拥有者消失时,Rust 会丢弃(drop)该值。丢弃一个值就必须释放该值拥有的任何其他值、堆存储和系统资源。丢弃可能发生在多种情况下:当变量超出作用域时;在表达式语句的末尾;当截断一个向量时,会从其末尾移除元素;等等

Deref 与 DerefMut

通过实现 std::ops::Deref trait 和 std::ops::DerefMut trait ,可以指定像 *. 这样的解引用运算符在你的类型上的行为

在Rust中,DerefDerefMut 是两个 trait,它们允许我们重载解引用运算符 **mut

1. Deref trait:它定义了一个叫做 deref 的方法,这个方法返回一个引用。当我们对一个实现了Deref trait的类型使用 * 运算符时,deref 方法会被自动调用,返回一个引用,如下例子

1
2
3
4
5
6
7
8
9
10
11
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

2. DerefMut trait:它定义了一个叫做 deref_mut 的方法,这个方法返回一个可变的引用。当我们对一个实现了 DerefMut trait 的类型使用 *mut 运算符时,deref_mut 方法会被自动调用,返回一个可变的引用

1
2
3
4
5
6
7
use std::ops::DerefMut;

impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

From 与 Into

std::convert::From trait 和 std::convert::Into trait 表示类型转换,这种转换会接受一种类型的值并返回另一种类型的值。AsRef trait 和 AsMut trait 用于从一种类型借入另一种类型的引用,而 FromInto 会获取其参数的所有权,对其进行转换,然后将转换结果的所有权返回给调用者

FromInto 的定义是对称的:

1
2
3
4
5
6
7
trait Into<T>: Sized {
fn into(self) -> T;
}

trait From<T>: Sized {
fn from(other: T) -> Self;
}

From 特质

From 特质用于定义一个类型如何从另一个类型转换而来。实现了 From 特质后,可以使用 from 函数来进行类型转换。这个特质通常用于定义清晰的转换逻辑,定义了一个结构体 Number,并实现了从 i32 到 Number 的转换

1
2
3
4
5
6
7
8
9
10
11
use std::convert::From;

struct Number {
value: i32,
}

impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}

Into 特质

Into 特质是 From 的互补特质。如果为类型 A 实现了 From<B>,那么同时也自动为 B 实现了 Into<A>。这意味着你可以使用 into 方法将类型 B 转换为类型 A。如,使用 frominto 方法来进行类型转换

1
2
3
4
5
6
7
8
fn main() {
let my_number = Number::from(30);
let int: i32 = 5;
let num: Number = int.into();

println!("My number is {}", my_number.value);
println!("Num is {}", num.value);
}

使用场景

  • 当你需要一个明确的转换方法时,使用 From
  • 当你需要为多种类型提供灵活的转换方式时,使用 Into

这两个特质在 Rust 的错误处理中尤其常见,例如将各种错误类型转换为统一的错误类型,使得错误处理更加统一和方便

注意!

From 和 Into 是不会失败的trait——它们的 API 要求这种转换不会失败。许多转换远比这复杂得多。例如,像 i64 这样的大整数可以存储比 i32 大得多的数值,如果没有一些额外的信息,那么将像 2_000_000_000_000i64 这样的数值转换成 i32 就没有多大意义。如果进行简单的按位转换,那么其中前 32 位就会被丢弃,通常不会产生我们预期的结果

1
2
3
let huge = 2_000_000_000_000i64;
let smaller = huge as i32;
println!("{}", smaller); // -1454759936

有很多选项可以处理这种情况。根据上下文的不同,“回绕型”转换可能比较合适。另外,像数字信号处理和控制系统这样的应用程序通常会使用“饱和型”转换,它会把比可能的最大值还要大的数值限制为最大值

TryFrom 与 TryInto

由于转换的行为方式不够清晰,因此 Rust 没有为 i32 实现 From<i64>,也没有实现任何其他可能丢失信息的数值类型之间的转换,而是为 i32 实现了 TryFrom<i64>TryFrom 和 TryInto 是 From 和 Into 的容错版“表亲”,这种转换同样是双向的,实现了 TryFrom 也就意味着实现了 TryInto

TryFrom 和 TryInto 的定义比 From 和 Into 稍微复杂一点儿

1
2
3
4
5
6
7
8
9
10
pub trait TryFrom<T>: Sized {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub trait TryInto<T>: Sized {
type Error;
fn try_into(self) -> Result<T, Self::Error>;
}

TryFrom 特质

TryFrom 特质用于定义一个可能失败的类型转换。如果转换可能因为某些原因失败(例如,超出范围、格式错误等),则使用 TryFrom。它返回一个 Result 类型,成功时包含目标类型,失败时包含错误信息。如这个例子中,SmallNumber 只接受 0 到 255 范围内的 i32 值。如果尝试从超出此范围的 i32 值转换,则会返回错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::convert::TryFrom;

struct SmallNumber {
value: u8,
}

impl TryFrom<i32> for SmallNumber {
type Error = String;

fn try_from(value: i32) -> Result<Self, Self::Error> {
if value >= 0 && value <= 255 {
Ok(SmallNumber { value: value as u8 })
} else {
Err("Number out of range".to_string())
}
}
}

TryInto 特质

TryInto 特质是 TryFrom 的互补特质。如果为类型 A 实现了 TryFrom<B>,那么同时也自动为 B 实现了 TryInto<A>。这意味着你可以使用 try_into 方法尝试将类型 B 转换为类型 A,并处理可能的错误。
在这个例子中,我们展示了如何使用 TryFromTryInto 来处理可能失败的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let large_number: i32 = 1000;
let small_number: Result<SmallNumber, _> = SmallNumber::try_from(large_number);

match small_number {
Ok(n) => println!("Small number is {}", n.value),
Err(e) => println!("Error: {}", e),
}

let another_number: i32 = 150;
let result: Result<SmallNumber, _> = another_number.try_into();

match result {
Ok(n) => println!("Small number is {}", n.value),
Err(e) => println!("Error: {}", e),
}
}

使用场景

  • 当输入数据的有效性不确定时,使用 TryFromTryInto 可以安全地尝试进行类型转换
  • 它们常用于处理外部数据,如用户输入、文件读取等,这些数据可能不满足我们的预期格式或范围

这两个特质提供了一种类型安全的方式来处理可能错误的转换,使得代码更加健壮和易于维护

From 和 Into 可以将类型与简单转换关联起来,而 TryFrom 和 TryInto 通过 Result 提供的富有表现力的错误处理扩展了 From 和 Into 的简单转换。这 4 个trait可以一起使用,在同一个 crate 中关联多个类型

小结

语言扩展 trait 已经了解了,里面有很多新的概念,虽然敲了示例代码,距离熟练掌握还有很长的路要走,还需多敲代码,在实践中夯实基础

欢迎大家讨论交流,如果喜欢本文章或感觉文章有用,动动你那发财的小手点赞、收藏、关注再走呗 ^_^ 

微信公众号:草帽Lufei
掘金:草帽lufei