14|类型系统:有哪些必须掌握的trait?

418 阅读9分钟

正式开始

已经学过的trait

  1. Clone / Copy trait,约定了数据被深拷贝和浅拷贝的行为;
  2. Read / Write trait,约定了对 I/O 读写的行为;
  3. Iterator,约定了迭代器的行为;
  4. Debug,约定了数据如何被以 debug 的方式显示出来的行为;
  5. Default,约定数据类型的缺省值如何产生的行为;
  6. From / TryFrom,约定了数据间如何转换的行为

内存相关:Clone / Copy / Drop

Clone trait


pub trait Clone {
  fn clone(&self) -> Self;

  fn clone_from(&mut self, source: &Self) {
    *self = source.clone()
  }
}
  1. Clone trait 有两个方法, clone() 和 clone_from() ,后者有缺省实现,所以平时我们只需要实现 clone() 方法即可
  2. 如果 a 已经存在,在 clone 过程中会分配内存,但是使用 a.clone_from(&b) 就可以避免内存分配,提高效率
  3. Clone trait 可以通过派生宏直接实现,如果在你的数据结构里,每一个字段都已经实现了 Clone trait,你可以用 #[derive(Clone)]
  4. Clone 是深度拷贝,栈内存和堆内存一起拷贝

值得注意的是,clone 方法的接口是 &self,这在绝大多数场合下都是适用的,我们在 clone 一个数据时只需要有已有数据的只读引用。 但对 Rc 这样在 clone() 时维护引用计数的数据结构,clone() 过程中会改变自己,所以要用 Cell 这样提供内部可变性的结构来进行改变

Copy trait

Copy trait 没有任何额外的方法,它只是一个标记 trait(marker trait)

pub trait Copy: Clone {}

  1. 如果要实现 Copy trait 的话,必须实现 Clone trait,然后实现一个空的 Copy trait

  2. 虽然没有任何行为,但它可以用作 trait bound 来进行类型安全检查,所以我们管它叫标记 trait

  3. 如果数据结构的所有字段都实现了 Copy,也可以用 #[derive(Copy)] 宏来为数据结构实现 Copy

  4. 如果类型实现了 Copy,那么在赋值、函数调用的时候,值会被拷贝,否则所有权会被移动

  5. 不可变引用实现了 Copy,而可变引用 &mut T 没有实现 Copy。为什么是这样?

    a. 因为如果可变引用实现了 Copy trait,那么生成一个可变引用然后把它赋值给另一个变量时,就会违背所有权规则:同一个作用域下只能有一个可变引用

Drop trait

pub trait Drop {
    fn drop(&mut self);
}
  1. 系统默认会依次对数据结构的每个域做 drop。但有两种情况你可能需要手工实现 Drop

    a. 第一种是希望在数据结束生命周期的时候做一些事情,比如记日志

    b. 第二种是需要对资源回收的场景

  2. Copy trait 和 Drop trait 是互斥的,两者不能共存,当你尝试为同一种数据类型实现 Copy 时,也实现 Drop,编译器就会报错

    a. Copy 是按位做浅拷贝,那么它会默认拷贝的数据没有需要释放的资源;而 Drop 恰恰是为了释放额外的资源而生的

标记 trait:Sized / Send / Sync / Unpin

协助编译器检查类型安全

Sized

  1. Sized trait 用于标记有具体大小的类型
  2. 在使用泛型参数时,Rust 编译器会自动为泛型参数加上 Sized 约束
  3. Rust 提供了 ?Sized 在少数情况下,将T约束为可变大小的类型

Send / Sync

pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}
  1. 这两个 trait 都是 unsafe auto trait
  2. auto 意味着编译器会在合适的场合,自动为数据结构添加它们的实现
  3. unsafe 代表实现的这个 trait 可能会违背 Rust 的内存安全准则,如果开发者手工实现这两个 trait ,要自己为它们的安全性负责

Send/Sync 是 Rust 并发安全的基础

  1. 如果一个类型 T 实现了 Send trait,意味着 T 可以安全地从一个线程移动到另一个线程,也就是说所有权可以在线程间移动

  2. 如果一个类型 T 实现了 Sync trait,则意味着 &T 可以安全地在多个线程中共享

    一个类型 T 满足 Sync trait,当且仅当 &T 满足 Send trait

对于 Send/Sync 在线程安全中的作用,可以这么看,

  1. 如果一个类型 T: Send,那么 T 在某个线程中的独占访问是线程安全的;
  2. 如果一个类型 T: Sync,那么 T 在线程间的只读共享是安全的

基本上原生数据结构都支持 Send / Sync,也就是说,绝大多数自定义的数据结构都是满足 Send / Sync 的。标准库中,不支持 Send / Sync 的数据结构主要有:

  1. 裸指针 *const T / *mut T。它们是不安全的,所以既不是 Send 也不是 Sync。
  2. UnsafeCell 不支持 Sync。也就是说,任何使用了 Cell 或者 RefCell 的数据结构不支持 Sync。
  3. 引用计数 Rc 不支持 Send 也不支持 Sync。所以 Rc 无法跨线程

多线程下使用Rc,RefCell,Arc

std::thread::spawn 定义

pub fn spawn<F, T>(f: F) -> JoinHandle<T> 
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

它的参数是一个闭包,这个闭包需要 Send + 'static:

  1. 'static 意思是闭包捕获的自由变量必须是一个拥有所有权的类型,或者是一个拥有静态生命周期的引用
  2. Send 意思是,这些被捕获自由变量的所有权可以从一个线程移动到另一个线程
如果在线程间传递 Rc<T>,是无法编译通过的

因为 Rc 的实现不支持 Send 和 Sync


// Rc 既不是 Send,也不是 Sync
fn rc_is_not_send_and_sync() {
    let a = Rc::new(1);
    let b = a.clone();
    let c = a.clone();
    thread::spawn(move || {
        println!("c= {:?}", c);
    });
}

image.png

如果在线程间传递 RefCell<T>,是可以工作的

RefCell 实现了 Send,但没有实现 Sync


fn refcell_is_send() {
    let a = RefCell::new(1);
    thread::spawn(move || {
        println!("a= {:?}", a);
    });
}
如果在线程间传递 Arc<T>,是无法编译通过的

// RefCell 现在有多个 Arc 持有它,虽然 Arc 是 Send/Sync,但 RefCell 不是 Sync
fn refcell_is_not_sync() {
    let a = Arc::new(RefCell::new(1));
    let b = a.clone();
    let c = a.clone();
    thread::spawn(move || {
        println!("c= {:?}", c);
    });
}
  1. Arc 内部的数据是共享的,需要支持 Sync 的数据结构,但是 RefCell 不是 Sync,编译失败
  2. 在多线程情况下,我们只能使用支持 Send/Sync 的 Arc ,和 Mutex 一起,构造一个可以在多线程间共享且可以修改的类型

use std::{
    sync::{Arc, Mutex},
    thread,
};

// Arc<Mutex<T>> 可以多线程共享且修改数据
fn arc_mutext_is_send_sync() {
    let a = Arc::new(Mutex::new(1));
    let b = a.clone();
    let c = a.clone();
    let handle = thread::spawn(move || {
        let mut g = c.lock().unwrap();
        *g += 1;
    });

    {
        let mut g = b.lock().unwrap();
        *g += 1;
    }

    handle.join().unwrap();
    println!("a= {:?}", a);
}

fn main() {
    arc_mutext_is_send_sync();
}

Unpin

用于自引用类型的

类型转换相关:From<T> / Into<T>/AsRef<T> / AsMut<T>

值类型到值类型的转换:From<T> / Into<T> / TryFrom<T> / TryInto<T>

From<T> / Into<T>

定义如下

pub trait From<T> {
    fn from(T) -> Self;
}

pub trait Into<T> {
    fn into(self) -> T;
}
  1. 在实现 From<T> 的时候会自动实现 Into<T>
  2. 大部分情况下,只用实现 From<T>,然后这两种方式都能做数据转换

// 实现 From 会自动实现 Into
impl<T, U> Into<U> for T where U: From<T> {
    fn into(self) -> U {
        U::from(self)
    }
}
  1. From 和 Into 还是自反的:把类型 T 的值转换成类型 T,会直接返回

// From(以及 Into)是自反的
impl<T> From<T> for T {
    fn from(t: T) -> T {
        t
    }
}

TryFrom<T> / TryInto<T>

  1. 如果你的数据类型在转换过程中有可能出现错误,可以使用 TryFrom<T> 和 TryInto<T>
  2. 它们的用法和 From<T> / Into<T> 一样,只是 trait 内多了一个关联类型 Error,且返回的结果是 Result<T, Self:Error>

引用类型到引用类型的转换:AsRef<T> / AsMut<T>

AsRef<T> / AsMut<T>


pub trait AsRef<T> where T: ?Sized {
    fn as_ref(&self) -> &T;
}

pub trait AsMut<T> where T: ?Sized {
    fn as_mut(&mut self) -> &mut T;
}
  1. 在 trait 的定义上,都允许 T 使用大小可变的类型,如 str、[u8] 等
  2. AsMut 除了使用可变引用生成可变引用外,其它都和 AsRef 一样
以std::fs::File::open为例
std::fs::File::open
  1. 它的参数 path 是符合 AsRef 的类型
  2. 可以为这个参数传入 String、&str、PathBuf、Path 等类型
  3. 当你使用 path.as_ref() 时,会得到一个 &Path

操作符相关:Deref / DerefMut

Rust 为所有的运算符都提供了 trait

image.png

Deref / DerefMut


pub trait Deref {
    // 解引用出来的结果类型
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}
  1. DerefMut “继承”了 Deref,只是它额外提供了一个 deref_mut 方法,用来获取可变的解引用

重点学习Deref

  1. 对于普通的引用,解引用很直观,因为它只有一个指向值的地址,从这个地址可以获取到所需要的值
let mut x = 42;
let y = &mut x;
// 解引用,内部调用 DerefMut(其实现就是 *self)
*y += 1;
  1. 对智能指针来说,拿什么域来解引用就不那么直观,以Rc为例

impl<T: ?Sized> Deref for Rc<T> {
    type Target = T;

    fn deref(&self) -> &T {
        // 最终指向了堆上的 RcBox 内部的 value 的地址
        // 然后如果对其解引用的话,得到了 value 对应的值
        &self.inner().value
    }
}

image.png

Deref 和 DerefMut 是自动调用的,*b 会被展开为 *(b.deref())

其它:Debug / Display / Default

Debug / Display


pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

Debug 和 Display 两个 trait 的签名一样,都接受一个 &self 和一个 &mut Formatter。那为什么要有两个一样的 trait 呢?

  1. Debug 是为开发者调试打印数据结构所设计的,而 Display 是给用户显示数据结构所设计
  2. Debug trait 的实现可以通过派生宏直接生成,而 Display 必须手工实现
  3. 在使用的时候,Debug 用 {:?} 来打印,Display 用 {} 打印

Default


pub trait Default {
    fn default() -> Self;
}
  1. Default trait 用于为类型提供缺省值
  2. 可以通过 derive 宏 #[derive(Default)] 来生成实现,前提是类型中的每个字段都实现了 Default trait
  3. 在初始化一个数据结构时,我们可以部分初始化,然后剩余的部分使用 Default::default()

小结

image.png

  1. 从数据的角度看,数据结构是具体数据的延迟绑定,泛型结构是具体数据结构的延迟绑定
  2. 从代码的角度看,函数是一组实现某个功能的表达式的延迟绑定,泛型函数是函数的延迟绑定
  3. trait 是行为的延迟绑定

推荐链接

  1. 使用clone_from避免内存分配
  2. Sized trait
  3. Send trait
  4. Sync trait
  5. Unpin trait
  6. Rc实现源码
  7. RefCell 实现了 Send,但没有实现 Sync
  8. 支持 Send/Sync 的 Arc
  9. From<T> trait
  10. Into<T> trait
  11. TryFrom<T> trait
  12. TryInto<T> trait
  13. std::fs::File:open
  14. 运算符 trait
  15. Deref trait
  16. DerefMut trait
  17. Debug trait
  18. Display trait
  19. Default trait
  20. LinkedList

精选问答

  1. Vec<T> 可以实现 Copy trait 么?为什么?

    a. Vec本身已经实现了Drop trait,和Copy trait是冲突的

  2. 在使用 Arc> 时,为什么下面这段代码可以直接使用 shared.lock()?


use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(1));
let mut g = shared.lock().unwrap();
*g += 1;

因为Arc实现了Deref,并未实现DeRefMut