likes
comments
collection
share

细聊Rust中关键字 move 对闭包的影响

作者站长头像
站长
· 阅读数 2

这篇文章主要记录了我在看了一篇关于Rust闭包的文章后引出的一系列思考与总结。

Understanding Closures in Rust.

在文章中,作者的结论如下:

  • 不捕获任何环境中变量的闭包可以转换为函数指针。这一点在The Rust ReferenceRust RFC中都有所提及。
  • 闭包的方法通过不可变引用访问其捕获的变量的闭包实现了Fn
  • 闭包的方法通过可变引用访问其捕获变量的闭包实现了FnMut
  • 闭包的方法若获取了只能被调用一次,即实现了FnOnce

并且作者画出了一张图来表示FnOnce、FnMut、Fn与所捕获的变量之间的关系:

细聊Rust中关键字 move 对闭包的影响

在这篇文章中,并未提及我感兴趣的关键字move对闭包的影响。此外我对作者开篇画出的图也有疑问。因此我觉得有必要深入思考有关闭包的一些问题。

闭包

闭包在Rust中的实现可以近似地理解为一个实现了FnOnce、FnMut和Fn其中一个trait的匿名结构体,这个匿名结构体保存捕获的环境中的变量。通过调用trait的方法来执行闭包体中的代码。

FnOnce、FnMut与Fn

先来看看标准库中三者的定义:

// FnOnce
#[lang = "fn_once"]
#[must_use = "closures are lazy and do nothing unless called"]
pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

// FnMut
#[lang = "fn_mut"]
#[must_use = "closures are lazy and do nothing unless called"]
pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

// Fn
#[lang = "fn"]
#[must_use = "closures are lazy and do nothing unless called"]
pub trait Fn<Args>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

从这三个trait的声明可以看出,Fn是FnMut的子trait,FnMut是FnOnce的子trait。也就是说实现了Fn的闭包一定实现了FnMut,同样,实现了FnMut的闭包一定实现了FnOnce。

闭包实现这三个trait的规则如下:

  • 所有的闭包都实现了FnOnce。
  • 如果闭包的方法移出了所捕获的变量的所有权,则只会实现FnOnce。
  • 如果闭包的方法没有移出所捕获的变量的所有权,并且对变量进行了修改,即通过可变借用使用所捕获的变量,则会实现FnMut。
  • 如果闭包的方法没有移出所捕获的变量的所有权,并且没有对变量进行修改,即通过不可变借用使用所捕获的变量,则会实现Fn。

图示:

细聊Rust中关键字 move 对闭包的影响

它们的关系如图:

细聊Rust中关键字 move 对闭包的影响

关键字move

关键字move的作用是将所引用的变量的所有权转移至闭包内,通常用于使闭包的生命周期大于所捕获的变量的原生命周期(例如将闭包返回或移至其他线程)。

捕获模式

闭包捕获环境中变量的模式为优先不可变借用,而后依次为唯一不可变借用(例如&&mut T),可变借用,移动。

  • 当闭包借用环境中的变量时,引用变量&T(或&mut T)将保存在闭包匿名结构体中。此时若想获取所引用的变量的所有权,就要使用move关键字将其所有权转移至闭包中,闭包会捕获所引用的变量本身T(或mut T),也就是下面所要说的情况。
  • 当闭包移动环境中的变量时,闭包会根据其语义进行Move或Copy。捕获的变量T(或mut T)将保存在闭包匿名结构体中。

那么,简单地来说,如果闭包捕获的变量为引用&T(或&mut T),使用关键字move后,闭包会根据所引用的对象的语义(Copy或Move)捕获T(或mut T)。

注意,在标准库文档和 The Rust Reference 中都明确说明了闭包实现FnOnce、FnMut和Fn中的哪个trait只与闭包如何使用所捕获的变量有关,与如何捕获变量无关关键字move影响的是闭包如何捕获变量,因此,对闭包实现FnOnce、FnMut和Fn没有任何影响

补充:如何捕获变量指的是闭包将捕获的变量以何种形式存储在匿名结构体中(值或引用),与闭包如何使用捕获的变量无关。

Copy trait

由于在使用关键字move后,闭包捕获的变量的所有权会发生变化,因此会对闭包产生另外一个影响,即闭包本身是否会实现Copy trait。

关于这一点其实根据捕获变量的语义很好判断。

  • 如果闭包捕获的变量为Copy语义,闭包会实现Copy trait
  • 如果闭包捕获的变量为Move语义,则闭包不会实现Copy trait

此外我们知道&T为Copy语义,&mut T为Move语义,再根据上面两条规则,就可以得出:

  • 闭包捕获的变量为不可变引用&T或Copy语义的T时,闭包会实现Copy trait
  • 闭包捕获的变量为可变引用&mut T或Move语义的T时,则闭包不会实现Copy trait

闭包是否实现Copy trait,只与捕获的变量是否可以被copy有关,与如何使用(是否修改捕获的变量)无关。

细聊Rust中关键字 move 对闭包的影响

一个例子

#[derive(Copy, Clone)]
struct FooCopy {
    value: i32,
}

impl FooCopy {
    fn new(value: i32) -> Self {
        Self { value }
    }
    
    fn get(&self) -> i32 {
        self.value
    }
    
    fn increase(&mut self) {
        self.value += 1;
    }
}

fn is_FnMut<F: FnMut()>(_closure: &F) {}

fn is_Copy<F: Copy>(_closure: &F) {}

fn main() {
    let mut foo_copy = FooCopy::new(0);
  
    let mut c_with_move = move || {
        for _ in 0..5 {
            foo_copy.increase();
        }
        
        println!("foo_copy in closure(with move): {}", foo_copy.get());
    };
    
    c_with_move();
    println!("foo_copy out of closure: {}\n", foo_copy.get());
    
    let mut c_without_move = || {
        for _ in 0..5 {
            foo_copy.increase();
        }
        
        println!("foo_copy in closure(without move): {}", foo_copy.get());
    };
    
    is_FnMut(&c_with_move);
    is_Copy(&c_with_move);
    
    is_FnMut(&c_without_move);
    //is_Copy(&c_without_move); // Error
    
    c_without_move();
    println!("foo_copy out of closure(without move): {}\n", foo_copy.get());
    
    c_with_move();
    println!("foo_copy out of closure(with move): {}\n", foo_copy.get());
}

输出:

foo_copy in closure(with move): 5
foo_copy out of closure: 0

foo_copy in closure(without move): 5
foo_copy out of closure(without move): 5

foo_copy in closure(with move): 10
foo_copy out of closure(with move): 5

例子中Copy语义的变量foo_copy在使用关键字move将其Copy至闭包c_with_move内后,对环境中的变量不再有影响。此时闭包的匿名结构体中保存的变量为mut FooCopy,在闭包中使用的increase()方法通过可变借用来进行操作,所以实现了FnMut + Copy trait。

在不使用关键字move时,闭包c_without_move对环境中的变量foo_copy进行了可变借用。此时闭包的匿名结构体内中保存的变量为&mut FooCopy,所以会对环境中的变量进行修改,其同样实现了FnMut trait,但不会实现Copy trait。

总结

  • 闭包实现FnOnce、FnMut和Fn中的哪个trait只与闭包如何使用所捕获的变量有关,与如何捕获变量无关。因此,关键字move不影响闭包实现FnOnce、FnMut和Fn
  • 在实际使用中,个人认为其实只需要考虑闭包方法是如何使用捕获的变量即可,几乎不需要考虑闭包本身是否实现Copy trait。关键字move主要用于使闭包摆脱所捕获的变量的生命周期限制,例如将闭包返回或移至其他线程时,必须使用move。

Quiz

最后,通过一个Quiz来加深理解,切记不要通过此图表来记忆,理解了相关概念后,也无需记忆。

细聊Rust中关键字 move 对闭包的影响

参考

引自: zhuanlan.zhihu.com/p/341815515