Rust 语言从入门到实战 唐刚 学习笔记11

基础篇 (11讲)

11|常见trait解析:标准库中的常见trait应该怎么用?

  • trait 在 Rust 中的重要性
  • trait 在 Rust 标准库中的应用。
  • Rust 标准库中含大量的 trait 定义,甚至 Rust 自身的某些语言特性就是在这些 trait 的帮助下实现的。
  • 这些 trait 和标准库里的各种类型一起,构成了整个 Rust 生态的根基,只有了解它们才算真正了解 Rust。

注:大量代码来自 Tour of Rust’s Standard Library Traits,加了必要的注解和分析。

标准库中的常用 trait

Default

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<T> 的 unwrap_or_default(),在类型参数上调用 default() 函数。

fn paint(color: Option<Color>) {
    // 如果没有颜色参数传进来,就用默认颜色
    let color = color.unwrap_or_default();
    // ...
}

// 由于default()是在trait中定义的关联函数,因此可方便的由类型参数调用
fn guarantee_length<T: Default>(mut vec: Vec<T>, min_len: usize) -> Vec<T> {
    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

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

ToString trait 定义。

trait ToString {
    fn to_string(&self) -> String;
}

一个 to_string() 方法,把各种类型实例转换成字符串。

不需要自己给类型实现 ToString trait,标准库已做了总实现(第 9 讲):

impl<T: Display> 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

Debug 跟 Display 很像,用于调试打印。

  • 打印要指定格式,Debug trait 配对 "{:?}" 格式,Display 配对 "{}" 。
  • 都将类型 表示或转换 成 String 类型。Debug 的排版信息比 Display 多一点,给程序员调试用,不给最终用户看。
  • Debug 还配套了一个美化版本格式 "{:#?}",把类型打印得更具结构化,适合调试的时候查看,如 json 结构会展开打印。
  • Rust 标准库提供了 Debug 宏。一般都以这个宏为目标类型自动生成 Debug trait,不自己手动去实现。标准库里没有 Display 宏,要手动实现Display trait。

Rust 类型能够自动被 derive 的条件:它里面的每个元素都能被 derive。

        如下面结构体里的每个字段,都是 i32 类型,基础类型在标准库里已经被实现过 Debug trait 了,可以直接在 Point 上做 derive 为 Point 类型实现 Debug trait。这个原则适用于所有 trait

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

PartialEq 和 Eq      (Partial:部分的,不完全的)

若类型上实现了 PartialEq,就能比较两个值是否相等。满足数学上的对称性传递性

  • 对称性(symmetry):a == b 导出 b == a。
  • 传递性(transitivity):a == b && b == c 导出 a == c。

Eq 定义为 PartialEq 的 subtrait,在 PartialEq 的对称性和传递性的基础上,添加了自反性

  • 自反性:对所有 a , 都有 a == a。

        最典型的: 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<T: PartialEq>(vec1: Vec<T>, vec2: Vec<T>) {
    if vec1 == vec2 {    // 比较
        // some code
    } else {
        // other code
    }
}

PartialOrd 和 Ord

  • PartialEq 只判断相等或不相等,
  • PartialOrd 进一步判断是小于、小于等于、大于还是大于等于。是为排序功能准备的。

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<T: Ord>(mut sortable: Vec<T>) -> Vec<T> {
    sortable.sort();
    sortable
}

运算符重载

Add trait,对加号(+)做自定义(运算符重载)。

Add 的定义:带一个类型参数 Rhs(可是任意名字),默认类型 Self,一个关联类型 Output,一个方法 add()。

trait Add<Rhs = Self> {
    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

按类似的方式练习如何自定义各种运算符。

Clone

定义:

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 一份生成一个新的所有权并被局部变量所持有。
  • 只拿到实例的引用,想拿到所有权,若类型实现了 Clone trait,就可 clone 一份拿到所有权。

clone() 是对象的深度拷贝,有较大的额外负载,不要担心,先跑通最重要。Rust 代码,性能一般不会太差。

注:浅拷贝按值拷贝一块连续的内存,只复制一层,不会去深究这个值里面是否有到其它内存资源的引用。与之相对,深拷贝会把这些引用对象递归全部拷贝

Rust 生态中,常看到 clone():把对实例引用的持有转换成了对对象所有权的持有

Copy

定义:

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<u32>,
}
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<u32>,    // 动态数组资源在堆内存中
}
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<u32>,    // 动态数组资源在堆内存中
  |     --------------- 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() 方法才会导致对象克隆,留下了足迹。
  • 类型实现了 Copy,用 = 号对实例再赋值时就发生了复制,缺少足迹。为潜在的 Bug 及性能降低埋下了隐患,审查时非常困难。

如果是.clone(),那只需搜索代码哪些地方出现了 clone 函数。

这个设计,在 Option<T> 和 Result<T, E> 的 unwrap() 系列函数上也有体现。

显式地留下足迹,是 Rust 语言设计重要的哲学之一

Copy 为什么要定义成 Clone 的 subtrait,而不是反过来?

Rust 鼓励优先使用 Clone 而不鼓励使用 Copy,在 derive Copy 时,也必须 derive Clone,多打几个字符。

Clone 和 Copy 在本质上其实是一样的,都是内存的按位复制,只是复制的规则有一些区别。

ToOwned

ToOwned 相当于 Clone 更宽泛的版本。

ToOwned 给类型提供了一个 to_owned() 方法,将引用转换为所有权实例

如:

let a: &str = "123456";
let s: String = a.to_owned();

 通过查看标准库和第三方库接口文档,以确定有没实现这个 trait。

Deref

Deref trait 用来把一种类型转换成另一种类型,但要在引用符号 &、点号操作符 . 或其他智能指针的触发下才会产生转换。如最常见的 &String 可自动转换到 &str(第 4 讲),因为 String 类型实现了 Deref trait。

 &Vec<T> 可自动转换为 &[T],因为 Vec[T] 实现了 Deref。

到这里,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

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 的具体使用。

闭包相关 trait

标准库有 3 个,FnOnce、FnMut、Fn。

// 闭包相关 trait
trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}
trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
trait Fn<Args>: FnMut<Args> {
    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 编译器在编译时确定,根据闭包捕获上下文环境变量时的行为来确定。

三种行为(⚠️ 所有权三态再现)。

  • 获取了上下文环境变量的所有权,对应 FnOnce。
  • 只获取了上下文环境变量的 &mut 引用,对应 FnMut。
  • 只获取了上下文环境变量的 & 引用,对应 Fn。

根据行为,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::<Vec<_>>();
    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::<Vec<_>>();
    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); // ✅
}

From<T> 和 Into<T>

关联的 trait From<T> 和 Into<T>,用于类型转换。

From<T> 把类型 T 转为自己,Into<T> 把自己转为类型 T。

// 
trait From<T> {
    fn from(T) -> Self;
}
trait Into<T> {
    fn into(self) -> T;
}

是互逆的 trait。Rust 只允许实现 From,自动实现了 Into,看标准库里的实现。

// 
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

对一个类型实现了 From 后,就可以像下面这样约束和使用

// 
fn function<T>(t: T)
where
    // 下面这两种约束是等价的
    T: From<i32>,
    i32: Into<T>
{
    // 等价
    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<T> 和 Into<T> 都隐含了所有权,From<T> 的 Self 是具有所有权的,Into<T> 的 T 也是具有所有权的。Into<T> 有个常用的比 From<T> 更自然的场景是,拿到了一个变量,想把它变成具有所有权的值,Into 写起来更顺手。 into() 是方法,而 from() 是关联函数

如:

// 
struct Person {
    name: String,
}
impl Person {
    // 这个方法只接收String参数
    fn new1(name: String) -> Person {
        Person { name }
    }
    // 这个方法可接收 
    // - String
    // - &String
    // - &str
    // - Box<str>
    // - char
    // 这几种参数,因为它们都实现了Into<String>
    fn new2<N: Into<String>>(name: N) -> Person {
        Person { name: name.into() }  // 调用into(),写起来很简洁
    }
}

TryFrom TryInto

TryFrom<T> 和 TryInto<T> 是 From<T> 和 Into<T> 的可失败版本。若认为转换可能会失败,就选这两个 trait 来实现。

// 
trait TryFrom<T> {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

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

调用 try_from() 和 try_into() 后返回 Result,要对 Result 进行处理。

FromStr

从字符串类型转换到自身。

// 
trait FromStr {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

这个 trait,就是字符串 parse() 方法背后的 trait。

// 
use std::str::FromStr;

fn example<T: FromStr>(s: &str) {
    // 下面4种表达等价
    let t: Result<T, _> = FromStr::from_str(s);
    let t = T::from_str(s);
    let t: Result<T, _> = s.parse();
    let t = s.parse::<T>(); // 最常用的写法
}

AsRef<T>

AsRef<T> 的定义类似下面:

// 
trait AsRef<T> {
    fn as_ref(&self) -> &T;
}

把自身的引用转换成目标类型的引用。

和 Deref 的区别,**deref()是隐式调用的,而as_ref()需要显式地调用 **。代码更清晰,出错的机会更少。

AsRef<T> 可以让函数参数中传入的类型更加多样化,引用类型、具有所有权的类型,都可以传递。如;

// 
// 使用 &str 作为参数可以接收下面两种类型
//  - &str
//  - &String
fn takes_str(s: &str) {
    // use &str
}
// 使用 AsRef<str> 作为参数可以接受下面三种类型
//  - &str
//  - &String
//  - String
fn takes_asref_str<S: 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<T>

小结

标准库里最常见的一些 trait,有个印象。

这些 trait 非常重要,它们一起构成了 Rust 生态宏伟蓝图的基础。很多前面讲到的一些神奇的“魔法”都在这节课揭开了面纱。

trait 设计给 Rust 带来了强大的表达力和灵活性,对它理解越深刻,越能体会 Rust 的厉害。

trait 完全解构了从 C++、Java 以来编程语言的发展范式,从紧耦合转换成松散的平铺式,让新特性的添加不会对语言本身造成沉重的负担。

第一阶段基础篇的学完了。-->Rust 语言里最重要的部分。

思考题

请举例说明 Deref 与 AsRef<T> 的区别。

请问下面这一句,能否只写Ord和Eq?Ord是PartialOrd的超集, Eq是PartialEq的超集。 编译器应该可以判断出,已经实现了Ord和Eq,当然也肯定实现了PartialOrd和PartialEq。 #[derive(Ord, PartialOrd, PartialEq, Eq)] // 注意这一句,4个都写上

作者回复: 不能,Rust编译器就是要让你多写一点。文中有说明类似的原因。主要是怕你滥用。

Deref 不能传递所有权变量,Asref可以传递所有权变量

作者回复: 有这个意思在里面,Deref需要通过其它操作符隐式触发,如 &, . 等,并且做的是自动 & 操作。

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值