Rust里的Fn/FnMut/FnOnce和闭包匿名函数关系

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

什么是闭包:闭包是引用了自由变量的函数。所以,闭包是一种特殊的函数。

在 Rust 中,Fn、FnMut 和 FnOnce 是三个用于表示闭包类型的 trait,每一个闭包都是实现了其中一个特性。闭包是一种可以捕获其环境变量的函数。在创建闭包是会默认实现这几个 trait 中的一个。

以下是三个 trait 的区别

Fn:Fn 是最基本的闭包 trait。它表示闭包可以捕获其环境变量的不可变引用。

FnMut:FnMut 表示闭包可以捕获其环境变量的可变引用。这意味着闭包可以修改其环境变量的值。

FnOnce:FnOnce 表示闭包只能调用一次。它表示闭包可以捕获其环境变量的所有权。这意味着闭包可以移动其环境变量的值。

先看function.rs源码:

rust 复制代码
pub trait FnOnce<Args: Tuple> {
    /// The returned type after the call operator is used.
    #[lang = "fn_once_output"]
    #[stable(feature = "fn_once_output", since = "1.12.0")]
    type Output;

    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args: Tuple>: FnOnce<Args> {
    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args: Tuple>: FnMut<Args> {
    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

也就是说实现FnMut的闭包肯定也实现了FnOnce;实现Fn的闭包同时肯定也实现了FnMut和FnOnce.

另外,从以上代码,我们还能这么理解:闭包可以看成一个有call方法的结构体。

现在,来看看rust圣经的3句话:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  • 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征

第一句没啥疑问,因为它是继承链的顶端,显然,所有闭包都实现了FnOnce

第二句,有些不明所以,先放着。

第三句,因为"不需要对捕获变量进行改变",可以理解为call(&self,所以规则上实现Fn没啥问题。

再看几个例子

为方便演示,我们定义几个函数:

rust 复制代码
fn exec_once<F: FnOnce()>(f: F){
    f();
}

fn exec_mut_fn<F: FnMut()>(mut mut_f: F){
    mut_f();
}

fn exec_fn<F: Fn()>(f: F){
    f();
}

依次用来执行实现各种Trait的闭包

例1,move了环境变量的闭包:

rust 复制代码
fn main() {
    let mut s = ">> ".to_string();
    let move_f = || println!("{}", s + " world");
    exec_once(move_f);
    //failed:
    // exec_fn(move_f);
    //failed:
    // exec_mut_fn(move_f);
}

例2:可变借用了环境变量的闭包(省略main):

rust 复制代码
    let mut_f = || { 
        s.push_str("hello");
        println!("{}", s);
    };
    exec_mut_fn(mut_f);
    // or:
    // exec_once(mut_f);

例3:不可变借用了环境变量的闭包:

rust 复制代码
    let f =  || println!("{}", s.len());
    exec_fn(f);
    //or
    //exec_mut_fn(f);
    //or
    //exec_once(f);

上面3个例子很好地解释了"继承关系"和"3条规则"。

继续绕:

例4:

rust 复制代码
fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

报错:

rust 复制代码
error[E0596]: cannot borrow `update_string` as mutable, as it is not declared as mutable
 --> src/main.rs:5:5
  |
4 |     let update_string =  |str| s.push_str(str);
  |         -------------          - calling `update_string` requires mutable binding due to mutable borrow of `s`
  |         |
  |         help: consider changing this to be mutable: `mut update_string`
5 |     update_string("hello");
  |     ^^^^^^^^^^^^^ cannot borrow as mutable

为什么update_string的类型都FnMut了,还不让动str呢?看看FnMut:

rust 复制代码
pub trait FnMut<Args: Tuple>: FnOnce<Args> {
    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

这里call_mut要求获得可变的self借用,这里self即update_string,所以,update_string得声明为可变才行。

好了,这就是全部......还有个例子:

let f =  move|| println!("{}", s.len());

猜猜看,上面的f哪几个exec能执行?

答案是都行~因为,其实这个move和前面的讨论并没太大关系,它意思是环境变量我都要move走,之后的代码就不能再用s了。f的类型只取决于闭包里怎么用s,而不取决于怎么捕获它,所以当然还是Fn咯~

相关推荐
qq_430583979 分钟前
QT笔记- QTreeView + QFileSystemModel 当前位置的保存与恢复 #选中 #保存当前索引
开发语言·笔记·qt
Crossoads16 分钟前
【汇编语言】外中断(一)—— 外中断的魔法:PC机键盘如何触发计算机响应
android·开发语言·数据库·深度学习·机器学习·计算机外设·汇编语言
Zik----18 分钟前
Anaconda搭建Python虚拟环境并在Pycharm中配置(小白也能懂)
开发语言·人工智能·python·机器学习·pycharm
大大怪将军~~~~19 分钟前
SpringBoot 入门
java·spring boot·后端
凯子坚持 c25 分钟前
解锁仓颉编程语言的奥秘:枚举类型、模式匹配与类接口全解析
开发语言·华为·harmonyos
小王爱吃月亮糖25 分钟前
QT-QVariant类应用
开发语言·c++·笔记·qt·visual studio
怒码ing35 分钟前
Java包装类型的缓存
java·开发语言·缓存
凡人的AI工具箱35 分钟前
每天40分玩转Django:Django缓存
数据库·人工智能·后端·python·缓存·django
安然望川海39 分钟前
springboot 使用注解设置缓存时效
spring boot·后端·缓存
问道飞鱼1 小时前
【Python知识】Python面向对象编程知识
开发语言·python·面向对象·