Rust 中 move、copy、clone、drop 语义和闭包捕获 Fn,FnMut,FnOnce

本文中的变量,指的是经过如下代码界说的常量 a 和变量 b。 实例指的是绑定到 a 的i32类型在 stack 内存的数据,和绑定到 b 变量的String类型在 stack 内存和 heap 内存中的数据。

let a = 0_u32;
let mut b = "Hello".to_string();

先说说运用场景

  • move、copy 的运用场景,主要是在变量赋值、函数调用的传入参数、函数回来值、闭包的变量捕获。
  • clone 需要显式调用。
  • drop 是在变量的作用规模完毕时,被主动调用。
  • 闭包中运用了外部变量,就会有闭包捕获。

move 语义

rust 中的类型,假如没有完成Copytrait,那么在此类型的变量赋值、函数入参、函数回来值都是 move 语义。这是与 c++ 的最大差异,从 c++11 开始,右值引证的出现,才有了 move 语义。但 rust 天生便是 move 语义。

如下的代码中,变量 a 绑定的String实例,被 move 给了 b 变量,尔后a变量便是不行访问了(编译器会帮助咱们查看)。然后 b 变量绑定的String实例又被 move 到了 f1 函数中,,b 变量就不行访问了。f1 函数对传入的参数做了必定的运算后,再将运算成果回来,这是函数 f1 的回来值被 move 到了 c 变量。在代码完毕时,只要 c 变量是有用的。

fn f1(s: String) -> String {
    s + " world!"
}
let a = String::from("Hello");
let b = a;
let c = f1(b);

留意,如上的代码中,String类型没有完成Copytrait,所以在变量传递的过程中,都是 move 语义。

copy 语义

rust 中的类型,假如完成了Copytrait,那么在此类型的变量赋值、函数入参、函数回来值都是copy语义。这也是 c++ 中默许的变量传递语义。

看看类似的代码,变量a绑定的i32实例,被 copy 给了b变量,尔后a、b变量同时有用,而且是两个不同的实例。然后 a 变量绑定的i32实例又被 copy 到了 f1 函数中,a变量依然有用。传入 f1 函数的参数 i 是一个新的实例,做了必定的运算后,再将运算成果回来。这时函数f1的回来值被 copy 到了 c 变量,同时f1函数中的运算成果作为临时变量也被毁掉(不会调用 drop,假如类型完成了Copytrait,就不能有Droptrait)。传入 b 变量调用 f1 的过程是相同的,仅仅回来值被 copy 给了d 变量。在代码完毕时,a、b、c、d 变量都是有用的。

fn f2(i: i32) -> i32 {
    i + 10
}
let a = 1_i32;
let b = a;
let c = f1(a);
let d = f1(b);

这儿再强调下,i32类型完成了Copytrait,所以整个变量传递过程,都是copy语义。

clone 语义

move 和 copy 语义都是隐式的,clone 需要显式的调用。

参阅类似的代码,变量 a 绑定的String实例,在赋值前先 clone 了一个新的实例,然后将新实例 move 给了 b 变量,尔后 a、b 变量同时有用。然后 b 变量在传入 f1 函数前,又 clone 一个新实例,再将这个新实例 move 到 f1 函数中。f1 函数对传入的参数做了必定的运算后,再将运算成果回来,这儿函数 f1 的回来值被 move 到了 c 变量。在代码完毕时,a、b、c 变量都是有用的。

fn f1(s: String) -> String {
    s + " world!"
}
let a = String::from("Hello");
let b = a.clone();
let c = f1(b.clone());

在这个过程中,在隐式 move 前,变量clone出新实例并将新实例move出去,变量自身坚持不变。

drop 语义

rust 的类型能够完成Droptrait,也能够不完成Droptrait。可是关于完成了Copytrait 的类型,不能完成Droptrait。也便是说CopyDrop两个 trait 对同一个类型只能有一个,鱼与熊掌不行兼得。

变量在脱离作用规模时,编译器会主动毁掉变量,假如变量类型有Droptrait,就先调用Drop::drop办法,做资源整理,一般会回收heap内存等资源,然后再回收变量所占用的 stack 内存。假如变量没有Droptrait,那就只回收 stack 内存。

正是因为在Drop::drop办法会做资源整理,所以CopyDroptrait 只能二选一。假如类型完成了Copytrait,在 copy 语义中并不会调用Clone::clone办法,不会做 deep copy,那就会出现两个变量同时具有一个资源(比如说是 heap 内存等),在这两个变量脱离作用规模时,会别离调用Drop::drop办法释放资源,这就会出现 double free 错误。

copy 与 clone 语义差异

先看看两者的界说:

pub trait Clone: Sized {
    fn clone(&self) -> Self;
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}
pub trait Copy: Clone {
    // Empty.
}

CloneCopy的 super trait,一个类型要完成Copy就有必要先完成Clone

再留意看,Copytrait 中没有任何办法,所以在 copy 语义中不行以调用用户自界说的资源复制代码,也便是不行以做 deep copy。copy 语义便是变量在stack内存的按位复制,没有其他任何剩余的操作。

Clone中有 clone 办法,用户能够对类型做自界说的资源复制,这就能够做 deep copy。在 clone 语义中,类型的Clone::clone办法会被调用,程序员在Clone::clone办法中做资源复制,同时在Clone::clone办法回来时,变量的 stack 内存也会被依照位复制一份,生成一个完整的新实例。

自界说类型完成CopyClonetrait

Clonetrait,关于任何自界说类型都能够完成。Copytrait只要自界说类型中的field悉数完成了Copytrait,才能够完成Copytrait。

如下代码举例,struct S1中的 field 别离是i32usize类型,都是有Copytrait,所以S1能够完成Copytrait。你能够经过#[derive(Copy, Clone)]办法完成,也能够自己写代码完成。

struct S1 {
    i: i32,
    u: usize,
}
impl Copy for S1 {}
impl Clone for S1 {
    fn clone(&self) -> Self {
        // 此处是S1的copy语义调用。
        // 正是i32和usize的Copy trait,才有了S1的Copy trait。
        *self   
    }
}

可是关于如下的struct S2,因为S2的field中有String类型,String类型没有完成Copytrait,所以S2类型就不能完成Copytrait。S2中也包含了E1类型,E1类型没有完成CloneCopytrait,可是咱们能够自己完成S2类型的Clonetrait,在Clone::clone办法中生成新的E1实例,这就能够 clone 出新的S2实例。

enum E1 {
    Text,
    Digit,
}
struct S2 {
    u: usize,
    e: E1,
    s: String,
}
impl Clone for S2 {
    fn clone(&self) -> Self {
        // 生成新的E1实例
        let e = match self.e {
            E1::Text => E1::Text,
            E1::Digit => E1::Digit,
        };
        Self {
            u: self.u,
            e,
            s: self.s.clone(),
        }
    }
}

留意,在这种情况下,不能经过#[derive(Clone)]主动完成S2类型的Clonetrait。只要类型中的一切 field 都有Clone,才能够经过#[derive(Clone)]主动完成Clonetrait。

闭包捕获变量

与闭包关联的是三个 trait 的界说,别离是FnOnceFnMutFn,界说如下:

pub trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}

留意三个 trait 中办法的 receiver 参数,FnOnceself参数,FnMut&mut self参数,Fn&self参数。

原则阐明如下:

  • 假如闭包仅仅对捕获变量的非修正操作,闭包捕获的是&T类型,闭包依照Fntrait 办法履行,闭包能够重复屡次履行。
  • 假如闭包对捕获变量有修正操作,闭包捕获的是&mut T类型,闭包依照FnMuttrait 办法履行,闭包能够重复屡次履行。
  • 假如闭包会耗费掉捕获的变量,变量被 move 进闭包,闭包依照FnOncetrait 办法履行,闭包只能履行一次。

关于完成Copytrait 和没有完成Copytrait 对类型,具体参阅如下对代码阐明。

1. 类型完成了Copy,闭包中是&T操作

如下的代码,f 闭包对 i 变量,没有修正操作,此处捕获到的是&i,所以 f 便是依照Fntrait 办法履行,能够屡次履行f。

fn test_fn_i8() {
    let mut i = 1_i8;
    let f = || i + 1;
    // f闭包对i是immutable borrowed,是Fn trait
    let v = f();
    // f闭包中仅仅immutable borrowed,此处能够再做borrowed。
    dbg!(&i);   
    // f能够调用屡次
    let v2 = f();
    // 此刻,f闭包生命周期现已完毕,i现已没有borrowed了,所以此处能够mutable borrowed。
    i += 10;
    assert_eq!(2, v);
    assert_eq!(2, v2);
    assert_eq!(11, i);
}

2. 类型完成了Copy,闭包中是&mut T操作

如下的代码,f 闭包对 i 变量,有修正操作,此处捕获到的是&mut i,所以f便是依照FnMuttrait 办法履行,留意 f 自身也是mut,能够屡次履行 f。

fn test_fn_mut_i8() {
    let mut i = 1_i8;
    let mut f = || {
        i += 1;
        i
    };
    // f闭包对i是mutable borrowed,是FnMut trait
    let v = f();
    // i现已被mutable borrowed,就不能再borrowed了。
    // dbg!(&i);   
    // f能够调用屡次
    let v2 = f();
    // 此刻,f闭包生命周期现已完毕,i没有mutable borrowed了,所以此处能够mutable borrowed。
    i += 10;    
    assert_eq!(2, v);
    assert_eq!(3, v2);
    assert_eq!(13, i);
}

3. 类型完成了Copy,闭包运用move关键字,闭包中是&mut T操作

如下的代码,f 闭包对i变量,有修正操作,而且运用了move关键字。因为i8完成了Copytrait,此处 i 会 copy 一个新实例,并将新实例 move 到闭包中,在闭包中的实践是一个新的i8变量。f 便是依照FnMuttrait 办法履行,留意 f 自身也是mut,能够屡次履行 f。

重点阐明,此处move关键字的运用,强制 copy 一个新的变量,将新变量move进闭包。

fn test_fn_mut_i8_move() {
    let mut i = 1_i8;
    let mut f = move || {
        i += 1;
        i
    };
    // i8有Copy trait,f闭包中是move进去的新实例,新实例不会被耗费,是FnMut trait
    let v = f();
    // i8有Copy trait,f闭包中是move进去的新实例,i没有borrowed,所以此处能够mutable borrowed。
    i += 10;    
    // f能够调用屡次
    let v2 = f();
    assert_eq!(2, v);
    assert_eq!(3, v2);
    assert_eq!(11, i);
}

4. 类型没有完成Copy,闭包中是&T操作

如下的代码,f 闭包对 s 变量,没有修正操作,此处捕获到的是&s,f 依照Fntrait 办法履行,能够屡次履行 f。

fn test_fn_string() {
    let mut s = "Hello".to_owned();
    let f = || -> String {
        dbg!(&s);
        "world".to_owned()
    };
    // f闭包对s是immutable borrowed,是Fn trait
    let v = f();
    // f闭包中是immutable borrowed,此处是第二个immutable borrowed。
    dbg!(&s);
    // f能够调用屡次
    let v2 = f();
    // f闭包生命周期完毕,s现已没有borrowed,所以此处能够mutable borrowed
    s += " moto";
    assert_eq!("world", &v);
    assert_eq!("world", &v2);
    assert_eq!("Hello moto", &s);
}

5. 类型没有完成Copy,闭包中是&mut T操作

如下的代码,f 闭包对 s 变量,调用push_str(&mut self, &str)办法修正,此处捕获到的是&mut s,f是依照FnMuttrait 办法履行,留意 f 自身是mut,f 能够屡次履行 f。

fn test_fn_mut_string() {
    let mut s = "Hello".to_owned();
    let mut f = || -> String {
        s.push_str(" world");
        s.clone()
    };
    // f闭包对s是mutable borrowed,是FnMut trait
    let v = f();
    // s是mutable borrowed,此处不能再borrowed。
    // dbg!(&s);
    // f能够屡次调用
    let v2 = f();
    // f闭包生命周期完毕,s现已没有borrowed,所以此处能够mutable borrowed
    s += " moto";
    assert_eq!("Hello world", &v);
    assert_eq!("Hello world world", &v2);
    assert_eq!("Hello world world moto", &s);
}

6. 类型没有完成Copy,闭包运用move关键字,闭包中是&mut T操作

如下的代码,f 闭包对 s 变量,调用push_str(&mut self, &str)办法修正,闭包运用move关键字,s 被move 进闭包,s 没有被耗费,f 是依照FnMuttrait 办法履行,留意f自身是mut,f 能够屡次履行。

fn test_fn_mut_move_string() {
    let mut s = "Hello".to_owned();
    let mut f = move || -> String {
        s.push_str(" world");
        s.clone()
    };
    // s被move进f闭包中,s没有被耗费,是FnMut trait
    let v = f();
    // s被move进闭包,s不能被borrowed
    // dbg!(&s);
    // f能够屡次调用
    let v2 = f();
    // s被move进闭包,s不能被borrowed,可是能够绑定新实例
    s = "moto".to_owned();
    assert_eq!("Hello world", &v);
    assert_eq!("Hello world world", &v2);
    assert_eq!("moto", &s);
}

7. 类型没有完成Copy,闭包中是&mut T操作,捕获的变量被耗费

如下的代码,f 闭包对 s 变量,调用push_str(&mut self, &str)办法修正,s 被闭包耗费,此处捕获到的是 s 自身,s 被 move 到闭包中,闭包外部 s 就不行见了。f 是依照FnOncetrait 办法履行,不行以屡次履行 f。

fn test_fn_once_string() {
    let mut s = "Hello".to_owned();
    let f = || -> String {
        s.push_str(" world");
        s   // s被耗费
    };
    // s被move进f闭包中,s被耗费,是FnOnce trait
    let v = f();
    // s变量现已被move了,不能再被borrowed
    // dbg!(&s);
    // f只能调用一次
    // let v2 = f();
    // s被move进闭包,s不能被borrowed,可是能够绑定新实例
    s = "moto".to_owned();
    assert_eq!("Hello world", v);
    assert_eq!("moto", &s);
}

8. 类型没有完成Copy,闭包运用move关键字,闭包中是T操作,捕获的变量被耗费

如下的代码,f 闭包对 s 变量,调用into_boxed_str(self)办法,s 被闭包耗费,此处捕获到的是 s 自身,s 被 move 到闭包中,闭包外部 s 就不行见了。f 是依照FnOncetrait 办法履行,不行以屡次履行 f。

本例中move关键字不是有必要的。

fn test_fn_once_move_string() {
    let mut s = "Hello".to_owned();
    let f = move || s.into_boxed_str();
    // s被move进f闭包中,s被耗费,是FnOnce trait
    let v = f();
    // s变量现已被move了,不能再被borrowed
    // dbg!(&s);
    // f只能调用一次
    // let v2 = f();
    // s被move进闭包,s不能被borrowed,可是能够绑定新实例
    s = "moto".to_owned();
    assert_eq!("Hello", &*v);
    assert_eq!("moto", &s);
}

最终总结

rust 中 move、copy、clone、drop 语义和闭包捕获是 rust 中基本的概念,代码过程中随时要清楚每个变量的变化。这会让自己的思路更明晰,rustc 也会变得温顺征服。