基础篇 (11讲)
注:大量代码来自 Tour of Rust’s Standard Library Traits,加了必要的注解和分析。
Default trait 的定义及对 Default trait 的实现和使用。
trait Default {
fn default() -> Self;
}
struct Color(u8, u8, u8);
impl Default for Color {
// 默认颜色是黑色 (0, 0, 0)
fn default() -> Self {
Color(0, 0, 0)
}
}
fn main() {
let color = Color::default();
// 或
let color: Color = Default::default();
}
其他一些地方也用到了 Default,如 Option
fn paint(color: Option) {
// 如果没有颜色参数传进来,就用默认颜色
let color = color.unwrap_or_default();
// ...
}
// 由于default()是在trait中定义的关联函数,因此可方便的由类型参数调用
fn guarantee_length(mut vec: Vec, min_len: usize) -> Vec {
for _ in 0..min_len.saturating_sub(vec.len()) {
vec.push(T::default()); // 这里用了 T::default() 这种形式
}
vec
}
若是 struct,还可用部分更新语法,这时其实是 Default 在发挥作用。
//
#[derive(Default)]
struct Color {
r: u8,
g: u8,
b: u8,
}
impl Color {
fn new(r: u8, g: u8, b: u8) -> Self {
Color {
r,
g,
b,
}
}
}
impl Color {
fn red(r: u8) -> Self {
Color {
r,
..Color::default() // 注意这一句
}
}
fn green(g: u8) -> Self {
Color {
g,
..Color::default() // 注意这一句
}
}
fn blue(b: u8) -> Self {
Color {
b,
..Color::default() // 注意这一句
}
}
}
Rust 标准库实际提供了标注,就是 #[derive()] 里面放 Default,方便为结构体自动实现 Default trait。
// 自动实现 Default trait
#[derive(Default)]
struct Color {
r: u8,
g: u8,
b: u8
}
#[derive(Default)]
struct Color2(u8, u8, u8);
注意细节:用 #[derive()] 在两个结构体上作了标注,这里面出现的这个 Default 不是 trait,它是一个同名的派生宏(后面会讲到)。这种派生宏标注帮助实现了 Default trait。Rustc 能正确区分 Default 到底是宏还是 trait,它们出现的位置不一样。
为什么可以自动实现 Default trait ?Color 里的类型是基础类型 u8,u8 是实现了 Default trait 的,默认值为 0。
Display trait 的定义。
// Display trait 的定义
trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
Display trait 对应于格式化符号 "{}",如 println!("{}", s),决定一个类型如何显示,其实是把类型转换成字符串表达。Display 要自己手动去实现。
如:
// Display 要自己手动去实现
use std::fmt;
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
// 为Point实现 Display
impl fmt::Display for Point {
// 实现唯一的fmt方法,这里定义用户自定义的格式
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y) // write!宏向stdout写入
}
}
fn main() {
println!("origin: {}", Point::default());
// 打印出 "origin: (0, 0)"
// 在 format! 中用 "{}" 将类型表示/转换为 String
let stringified = format!("{}", Point::default());
assert_eq!("(0, 0)", stringified); // ✅
}
ToString trait 定义。
trait ToString {
fn to_string(&self) -> String;
}
一个 to_string() 方法,把各种类型实例转换成字符串。
不需要自己给类型实现 ToString trait,标准库已做了总实现(第 9 讲):
impl ToString for T
凡实现了 Display 的就实现了 ToString。
这两个功能本质一样:把类型转换成字符串表达。Display 侧重展现,ToString 侧重类型转换。
证明这两者是等价的:
#[test] // ✅
fn display_point() {
let origin = Point::default();
assert_eq!(format!("{}", origin), "(0, 0)");
}
#[test] // ✅
fn point_to_string() {
let origin = Point::default();
assert_eq!(origin.to_string(), "(0, 0)");
}
#[test] // ✅
fn display_equals_to_string() {
let origin = Point::default();
assert_eq!(format!("{}", origin), origin.to_string());
}
把一个符合条件的类型实例转换成字符串有两种常用方法。
let s = format!("{}", obj);
// 或
let s = obj.to_string();
Debug 跟 Display 很像,用于调试打印。
Rust 类型能够自动被 derive 的条件:它里面的每个元素都能被 derive。
如下面结构体里的每个字段,都是 i32 类型,基础类型在标准库里已经被实现过 Debug trait 了,可以直接在 Point 上做 derive 为 Point 类型实现 Debug trait。这个原则适用于所有 trait。
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
若类型上实现了 PartialEq,就能比较两个值是否相等。满足数学上的对称性和传递性:
Eq 定义为 PartialEq 的 subtrait,在 PartialEq 的对称性和传递性的基础上,添加了自反性,
最典型的: Rust 的浮点数只实现了 PartialEq,没实现 Eq(据 IEEE 的规范,浮点数中存在一个 NaN,NaN ≠ NaN )。对整数来说,PartialEq 和 Eq 都实现了。
若类型的所有字段都实现了 PartialEq,那用标准库中定义的 PartialEq 派生宏,可以为目标类型自动实现可比较能力,用 == 号,或用 assert_eq!() 做判断。
#[derive(PartialEq, Debug)] // 注意这一句
struct Point {
x: i32,
y: i32,
}
fn example_assert(p1: Point, p2: Point) {
assert_eq!(p1, p2); // 比较
}
fn example_compare_collections(vec1: Vec, vec2: Vec) {
if vec1 == vec2 { // 比较
// some code
} else {
// other code
}
}
PartialOrd 定义为 PartialEq 的 subtrait。可在类型上用过程宏一起 derive 实现。
#[derive(PartialEq, PartialOrd)]
struct Point {
x: i32,
y: i32,
}
#[derive(PartialEq, PartialOrd)]
enum Stoplight {
Red,
Yellow,
Green,
}
Ord 定义为 Eq + PartialOrd 的 subtrait。
若为类型实现了 Ord,那对该类型的所有值,可以做出一个严格的总排序,如 u8,可严格地从 0 排到 255,形成确定的从小到大的序列。
同样的,浮点数实现了 PartialOrd,但是没实现 Ord。
由于 Ord 严格的顺序性,若一个类型实现了 Ord,那该类型可被用作 BTreeMap 或 BTreeSet 的 key。
BTreeMap、BTreeSet:相对于 HashMap 和 HashSet,是两种可排序结构。
示例:
use std::collections::BTreeSet;
#[derive(Ord, PartialOrd, PartialEq, Eq)] // 注意这一句,4个都写上
struct Point {
x: i32,
y: i32,
}
fn example_btreeset() {
let mut points = BTreeSet::new();
points.insert(Point { x: 0, y: 0 }); // 作key值插入
}
// 实现了Ord trait的类型的集合,可调用 .sort() 排序方法
fn example_sort(mut sortable: Vec) -> Vec {
sortable.sort();
sortable
}
Add trait,对加号(+)做自定义(运算符重载)。
Add 的定义:带一个类型参数 Rhs(可是任意名字),默认类型 Self,一个关联类型 Output,一个方法 add()。
trait Add {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
使用示例:
struct Point {
x: i32,
y: i32,
}
// 为 Point 类型实现 Add trait,这样两个Point实例就可以直接相加
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2; // 这里直接用+号作用在两个Point实例上
assert_eq!(p3.x, p1.x + p2.x); // ✅
assert_eq!(p3.y, p1.y + p2.y); // ✅
}
Rust 标准库提供了一套完整的与运算符对应的 trait:可重载的运算符。
std::ops - Rust
按类似的方式练习如何自定义各种运算符。
定义:
trait Clone {
fn clone(&self) -> Self;
}
给目标类型提供 clone() 方法用来完整地克隆实例。用标准库提供的 Clone 派生宏可方便地为目标类型实现 Clone trait。
如:
#[derive(Clone)]
struct Point {
x: u32,
y: u32,
}
每一个字段(u32 类型)都实现了 Clone,通过 derive,自动为 Point 类型实现了 Clone trait。实现后,Point 的实例 point 使用 point.clone() 就可以把自己克隆一份了。
看方法的签名,用的是实例的不可变引用。
fn clone(&self) -> Self;
有两种情况。
clone() 是对象的深度拷贝,有较大的额外负载,不要担心,先跑通最重要。Rust 代码,性能一般不会太差。
注:浅拷贝是按值拷贝一块连续的内存,只复制一层,不会去深究这个值里面是否有到其它内存资源的引用。与之相对,深拷贝就会把这些引用对象递归全部拷贝。
Rust 生态中,常看到 clone():把对实例引用的持有转换成了对对象所有权的持有。
定义:
trait Copy: Clone {}
Copy定义为 Clone 的 subtrait,不包含任何内容,仅是个标记(marker)。不能为自定义类型实现这个 trait。如:
impl Copy for Point {} // 这是不行的
用 Rust 标准库的 Copy 过程宏,自动为目标类型实现 Copy trait。
#[derive(Copy, Clone)]
struct SomeType;
Copy 是 Clone 的 subtrait。所以理所当然要把 Clone trait 也一起实现,这里一次性 derive 过来。
Copy 和 Clone 的区别:Copy 浅拷贝只复制一层,不会去深究这个值里面是否有到其他内存资源的引用,如一个字符串的动态数组。
struct Atype {
num: u32,
a_vec: Vec,
}
fn main() {
let a = Atype {
num: 100,
a_vec: vec![10, 20, 30],
};
let b = a; // 这里发生了移动
}
第 10 行将 a 的所有权移动给 b(第 2 讲)。
若结构体实现了 Clone trait ,可调用.clone() 来产生一份新的所有权。
#[derive(Clone, Debug)]
struct Atype {
num: u32,
a_vec: Vec, // 动态数组资源在堆内存中
}
fn main() {
let a = Atype {
num: 100,
a_vec: vec![10, 20, 30],
};
let mut b = a.clone(); // 克隆,也将堆内存中的Vec资源部分克隆了一份
b.num = 200; // 更改b的值
b.a_vec[0] = 11;
b.a_vec[1] = 21;
b.a_vec[2] = 31;
println!("{a:?}"); // 对比两份值
println!("{b:?}");
}
// 输出
Atype { num: 100, a_vec: [10, 20, 30] }
Atype { num: 200, a_vec: [11, 21, 31] }
clone() 一份新的所有权出来,b 改动的值不影响 a 的值。
想在 Atype 上实现 Copy trait ,会报错。
error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:1:10
|
1 | #[derive(Copy, Clone, Debug)]
| ^^^^
...
4 | a_vec: Vec, // 动态数组资源在堆内存中
| --------------- this field does not implement `Copy`
说动态数组字段 a_vec 没有实现 Copy trait,不能对 Atype 实现 Copy trait。
原因:Vec 是一种所有权结构,若在它上面实现了 Copy,再赋值时,会出现对同一份资源的两个指向,冲突了!
若一个类型实现了 Copy,具备一个特别重要的特性:再赋值的时候会复制一份自身(新创建一份所有权)。看下面这个 值全在栈上 的类型。
#[derive(Clone)]
struct Point {
x: u32,
y: u32,
}
fn main() {
let a = Point {x: 10, y: 10};
let b = a; // 这里发生了所有权move,a在后续不能使用了
}
对 Point 实现 Clone 和 Copy。
#[derive(Copy, Clone)]
struct Point {
x: u32,
y: u32,
}
fn main() {
let a = Point {x: 10, y: 10};
let b = a; // 这里发生了复制,a在后续可以继续使用
let c = a; // 这里又复制了一份,这下有3份了
}
第 2 讲,复制与移动的语义区别根源
Point 结构体里面的字段其实全都是固定尺寸的,并且 u32 是 copy 语义的,按理说 Point 也是编译时已知固定尺寸的,为什么它默认不实现 copy 语义呢?
Rust 设计者故意这么做的。因为 Copy trait 其实关联到赋值语法,仅仅从这个语法(let a = b;),很难一下子看出来这到底是 copy 还是 move,是一种隐式行为。
在所有权的第一设计原则框架下,Rust 默认选择了 move 语义。方便起见,Rust 只让最基础的那些类型,如 u32、bool 等具有 copy 语义。用户自定义的类型,一概默认 move 语义。若想给自定义类型赋予 copy 语义,要显式地在类型上添加 Copy 的 derive。
如果是.clone(),那只需搜索代码哪些地方出现了 clone 函数。
这个设计,在 Option
Copy 为什么要定义成 Clone 的 subtrait,而不是反过来?
Rust 鼓励优先使用 Clone 而不鼓励使用 Copy,在 derive Copy 时,也必须 derive Clone,多打几个字符。
Clone 和 Copy 在本质上其实是一样的,都是内存的按位复制,只是复制的规则有一些区别。
ToOwned 相当于 Clone 更宽泛的版本。
ToOwned 给类型提供了一个 to_owned() 方法,将引用转换为所有权实例。
如:
let a: &str = "123456";
let s: String = a.to_owned();
通过查看标准库和第三方库接口文档,以确定有没实现这个 trait。
Deref trait 用来把一种类型转换成另一种类型,但要在引用符号 &、点号操作符 . 或其他智能指针的触发下才会产生转换。如最常见的 &String 可自动转换到 &str(第 4 讲),因为 String 类型实现了 Deref trait。
&Vec
到这里,Rust 里很多魔法就开始揭开神秘面纱了。
有了这些 trait 及在各种类型上的实现,Rust 可以写出顺应直觉、赏心悦目、功能强大的代码。
在标准库文档中搜索 Deref,查阅所有实现了 Deref trait 的 implementors。
提醒:尝试用 Deref 机制去实现 OOP 继承,那是徒劳和不完整的,有兴趣的话看链接。
https://github.com/pretzelhammer/rust-blog/blob/master/posts/tour-of-rusts-standard-library-traits.md#deref--derefmut
Drop trait 给类型做自定义垃圾清理(回收)。
trait Drop {
fn drop(&mut self);
}
实现了这个 trait 的类型的实例在走出作用域的时候,触发调用 drop() 方法,这个调用发生在这个实例被销毁之前。
struct A;
impl Drop for A {
fn drop(&mut self){
// 可以尝试在这里打印点东西看看什么时候调用
}
}
一般不需为自己的类型实现这个 trait 。特殊情况,如要调用外部的 C 库函数,在 C 那边分配了资源,由 C 库里的函数负责释放,这时要在 Rust 的包装类型(对 C 库中类型的包装)上实现 Drop,并调用那个 C 库中释放资源的函数。课程最后两讲 FFI 编程中,你会看到 Drop 的具体使用。
标准库有 3 个,FnOnce、FnMut、Fn。
// 闭包相关 trait
trait FnOnce {
type Output;
fn call_once(self, args: Args) -> Self::Output;
}
trait FnMut: FnOnce {
fn call_mut(&mut self, args: Args) -> Self::Output;
}
trait Fn: FnMut {
fn call(&self, args: Args) -> Self::Output;
}
闭包就是一种能捕获上下文环境变量的函数。
// 能捕获上下文环境变量的函数
let range = 0..10;
let get_range_count = || range.count();
get_range_count 就是闭包,range 是被闭包捕获的环境变量。
不通过 fn 定义。在 Rust 中,闭包的类型不是 fn 这种函数指针类型,有单独的类型定义。
具体是什么类型呢?其实我们也不知道。闭包的类型由 Rust 编译器在编译时确定,根据闭包捕获上下文环境变量时的行为来确定。
三种行为(⚠️ 所有权三态再现)。
根据行为,Rust 编译时把闭包生成为三种类型之一。这三种不同类型的闭包,具体类型形式不知道,Rust 没有暴露给我们。Rust 暴露了 FnOnce、FnMut、Fn 这 3 个 trait,对应三种类型。结合我们前面讲到的 trait object,就能在我们的代码中对那些类型进行描述了。
FnOnce 闭包类型只能被调用一次:
//
fn main() {
let range = 0..10;
let get_range_count = || range.count();
assert_eq!(get_range_count(), 10); // ✅
get_range_count(); // ❌
}
再调用就报错。
FnMut 闭包类型能被调用多次,且能修改上下文环境变量的值,不过有副作用,可能会导致错误或者不可预测的行为。如:
//
fn main() {
let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
let mut min = i32::MIN;
let ascending = nums.into_iter().filter(|&n| {
if n <= min {
false
} else {
min = n; // 这里修改了环境变量min的值
true
}
}).collect::>();
assert_eq!(vec![0, 4, 8, 10, 15, 18], ascending); // ✅
}
Fn 类闭包能被调用多次,对上下文环境变量没有副作用:
//
fn main() {
let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
let min = 9;
let greater_than_9 = nums.into_iter().filter(|&n| n > min).collect::>();
assert_eq!(vec![10, 15, 18, 13], greater_than_9); // ✅
}
另外,fn 这种函数指针,用在不需要捕获上下文环境变量的场景,如:
//
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let mut fn_ptr: fn(i32) -> i32 = add_one; // 注意这里的类型定义
assert_eq!(fn_ptr(1), 2); // ✅
// 如果一个闭包没有捕捉环境变量,它可以通过类型转换转成 fn 类型
fn_ptr = |x| x + 1; // same as add_one
assert_eq!(fn_ptr(1), 2); // ✅
}
关联的 trait From
From
//
trait From {
fn from(T) -> Self;
}
trait Into {
fn into(self) -> T;
}
是互逆的 trait。Rust 只允许实现 From,自动实现了 Into,看标准库里的实现。
//
impl Into for T
where
U: From,
{
fn into(self) -> U {
U::from(self)
}
}
对一个类型实现了 From 后,就可以像下面这样约束和使用
//
fn function(t: T)
where
// 下面这两种约束是等价的
T: From,
i32: Into
{
// 等价
let example: T = T::from(0);
let example: T = 0.into();
}
一个具体的例子。
//
struct Point {
x: i32,
y: i32,
}
impl From<(i32, i32)> for Point { // 实现从(i32, i32)到Point的转换
fn from((x, y): (i32, i32)) -> Self {
Point { x, y }
}
}
impl From<[i32; 2]> for Point { // 实现从[i32; 2]到Point的转换
fn from([x, y]: [i32; 2]) -> Self {
Point { x, y }
}
}
fn example() {
// 使用from()转换不同类型
let origin = Point::from((0, 0));
let origin = Point::from([0, 0]);
// 使用into()转换不同类型
let origin: Point = (0, 0).into();
let origin: Point = [0, 0].into();
}
其实 From 是单向的。两个类型要互转,需要互相实现 From 。
本身,From
如:
//
struct Person {
name: String,
}
impl Person {
// 这个方法只接收String参数
fn new1(name: String) -> Person {
Person { name }
}
// 这个方法可接收
// - String
// - &String
// - &str
// - Box
// - char
// 这几种参数,因为它们都实现了Into
fn new2>(name: N) -> Person {
Person { name: name.into() } // 调用into(),写起来很简洁
}
}
TryFrom
//
trait TryFrom {
type Error;
fn try_from(value: T) -> Result;
}
trait TryInto {
type Error;
fn try_into(self) -> Result;
}
调用 try_from() 和 try_into() 后返回 Result,要对 Result 进行处理。
从字符串类型转换到自身。
//
trait FromStr {
type Err;
fn from_str(s: &str) -> Result;
}
这个 trait,就是字符串 parse() 方法背后的 trait。
//
use std::str::FromStr;
fn example(s: &str) {
// 下面4种表达等价
let t: Result = FromStr::from_str(s);
let t = T::from_str(s);
let t: Result = s.parse();
let t = s.parse::(); // 最常用的写法
}
AsRef
//
trait AsRef {
fn as_ref(&self) -> &T;
}
把自身的引用转换成目标类型的引用。
和 Deref 的区别,**deref()是隐式调用的,而as_ref()需要显式地调用 **。代码更清晰,出错的机会更少。
AsRef
//
// 使用 &str 作为参数可以接收下面两种类型
// - &str
// - &String
fn takes_str(s: &str) {
// use &str
}
// 使用 AsRef 作为参数可以接受下面三种类型
// - &str
// - &String
// - String
fn takes_asref_str>(s: S) {
let s: &str = s.as_ref();
// use &str
}
fn example(slice: &str, borrow: &String, owned: String) {
takes_str(slice);
takes_str(borrow);
takes_str(owned); // ❌
takes_asref_str(slice);
takes_asref_str(borrow);
takes_asref_str(owned); // ✅
}
本例,具有所有权的 String 字符串也可以直接传入参数中了,相对于 &str 的参数类型表达更加扩展了一步。
可把 Deref 看成 隐式化(或自动化)+ 弱化版本的 AsRef
标准库里最常见的一些 trait,有个印象。
这些 trait 非常重要,它们一起构成了 Rust 生态宏伟蓝图的基础。很多前面讲到的一些神奇的“魔法”都在这节课揭开了面纱。
trait 设计给 Rust 带来了强大的表达力和灵活性,对它理解越深刻,越能体会 Rust 的厉害。
trait 完全解构了从 C++、Java 以来编程语言的发展范式,从紧耦合转换成松散的平铺式,让新特性的添加不会对语言本身造成沉重的负担。
第一阶段基础篇的学完了。-->Rust 语言里最重要的部分。
请举例说明 Deref 与 AsRef
请问下面这一句,能否只写Ord和Eq?Ord是PartialOrd的超集, Eq是PartialEq的超集。 编译器应该可以判断出,已经实现了Ord和Eq,当然也肯定实现了PartialOrd和PartialEq。 #[derive(Ord, PartialOrd, PartialEq, Eq)] // 注意这一句,4个都写上
作者回复: 不能,Rust编译器就是要让你多写一点。文中有说明类似的原因。主要是怕你滥用。
Deref 不能传递所有权变量,Asref可以传递所有权变量
作者回复: 有这个意思在里面,Deref需要通过其它操作符隐式触发,如 &, . 等,并且做的是自动 & 操作。