searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Rust入门(八) —— 理解闭包

2024-05-23 07:35:39
106
0

写(无)在(关)前(紧)面(要)的话

原计划会先写完Rust宏编程,但最近工作实在抽不出手,而且过程宏需要有一些编译理论支撑(比如抽象语法树), 还没有想好怎么用比较简单易懂的方式把这个讲清楚(主要是自己还没理清楚)。

最近在改一些项目工程,在实际项目中,闭包和迭代使用也是极其频繁;并且,在rust函数式链式调用过程中,配合闭包的使用,可以写出非常简洁的代码,艺术成分很高(恐怕有三四楼那么高啦)。另一方面,在工程中,去阅读别人写的这样的代码时,如果对这块内容掌握不太熟练,难免会觉得晦涩,比如下面这段代码:

如截图的函数,对入参engin_state进行一些列链式处理,中间掺杂了三次闭包用于数据处理和映射转换;看是一脉相承,但,于数据而言,却是“轻舟已过万重山”!

如果尝试去理解每一个转变的细节(当需要去理解数据处理细节、调整之前的数据转换模式时)就必须熟练掌握 Option工具套件、Result工具套件、闭包处理原理、迭代工具套件。

本着学以致用的目的,我们就随遇而安吧;遇都遇上了,就先整闭包吧,宏编程下集就下期再补......

闭包的应用场景

(1)封装私有变量和方法

通过闭包,可以创建私有变量和函数,只需要暴露必要的接口给外包,从而实现信息隐藏和封装。最经典的就是迭代器,迭代器通过暴露少量接口(iter,next 等)来支持数据循环获取,但隐藏了内部数据存储、获取、迭代的实现细节。

(2)函数式编程

闭包在函数式编程中是非常有用的工具,可以用于创建数据过滤、数据映射、高阶函数、延迟函数等。

(3)作为入参或回调函数

闭包可以作为入参传递到函数内部,在合适的时机进行执行;属于标准的注册、回调模型。这有利于实现异步处理中,状态存储和关联(相比于一般函数)。比如,在事件处理的回调函数中传递闭包,该闭包可能会携带事件初始化时的一些环境信息,这有助于提高事件处理过程中的信息完备性。

(4)数据缓存

利用闭包可以缓存计算结果,避免重复计算,从而提高性能。

 

总的来说,闭包在Rust中有着广泛的应用,能够帮助开发人员编写更加模块化、灵活和高效的代码(这句话感觉像是AI生成的呢)。

闭包的语法

一个典型的闭包,如下面示例:

let inc= |x:i32| {
    let mut result: i32 = x;
    result += 1;
    result
};

一个闭包通常包括两部分:其一,由“||”所包裹的参数描述;其二,有“{}”所包裹的表达式区域。如下:

| 【参数区域】| {【表达式区域】}

参数区域:描述0个或多个闭包函数的参数;参数描述与函数参数描述类似, 【参数名称】:【参数类型】,如果多个参数用 分割。

表达式区域:描述闭包的逻辑实现,其中最后一个表达式的值作为闭包的返回值。

注意:

(1)在书写闭包时,通常会省略闭包参数的类型。这是因为闭包通常运行在函数内部,存在上下文,rust编辑器可以进行类型推导。值得强调的是,闭包的参数(返回值)推导第一次完成后就固定了,不可变更,属于编译时的行为,而非运行时的行为。例如:

let self_x= |x| x;

let s = self_x(String::from("hello"));
let n = self_x(5);

上述代码,编译运行时报错:在编译self_x(5) 提示类型不匹配(期望是String,而传入i32)。原因是该闭包在 上一句编译时就完成类型推导:闭包的入参和返回都是String类型。

(2)如果表达式内容本身为一句话表达式,则可以省略“{}”。例如 上面示例中的self_x。另外,本节开始的inc闭包可以简化为:

let inc = |x| x+1;

捕获变量

闭包与函数的另一个重要的差异在于,闭包可以捕获其环境中的值(变量)。之所以把它称为“闭包”是因为它们“包含在环境中”(close over their environment)。

例如:

// 变量捕获案例01 
#[test]
fn test_closer_ref_use_env_var() {
        let mut num = 5;
        let num_plus = |x: i32| {
            num + x                        // 这里使用了外部的num变量
        };
        assert_eq!(num_plus(1), 6);
}

上述代码可以通过测试,num_plus捕获并使用环境中的num的值,与参数x运算后返回。

对应于rust函数参数传递的:引用传递,可变引用传递,值传递(移交所有权)三种模式,闭包在捕获环境中的变量时也有三种模式:

(1)不可变引用方式捕获

(2)可变引用方式捕获

(3)所有权方式捕获

接下来,我们逐一探讨。

以不可变引用的方式捕获环境变量

对案例01代码改进,尝试在闭包存活期间,修改环境中的变量num:

// 变量捕获案例02
#[test]
fn test_closer_ref_use_env_var() {
        let mut num = 5;

        let num_plus = |x: i32| {
            num + x
        };
        assert_eq!(num_plus(1), 6);
        assert_eq!(num, 5);

        num = 10;
        assert_eq!(num_plus(1), 11);
}

乍一看,上面的代码没有问题,但编译运行时会报错:

原因是: 在闭包中,num被借用(不可变借用),而在次期间尝试对num进行修改,进而违反了Rust的借用检测要求。

注意:当变量被闭包借用后,其引用关系会保持在闭包函数变量的生命周期内(案例中num_plus),在一些场景中,会导致引用被意外持有而影响数据更新!

案例02中, 对num进行后续赋值会报错,原因是num_plus的生命周期在此刻还未结束,删除最后一行(解除对num_plus引用,使得num_plus在num=10前结束生命周期,解除对num的引用),则num变得可以修改

// 变量捕获案例03
#[test]
fn test_closer_ref_use_env_var() {
        let mut num = 5;

        let num_plus = |x: i32| {
            num + x
        };
        assert_eq!(num_plus(1), 6);
        assert_eq!(num, 5);

        num = 10;
        // assert_eq!(num_plus(1), 11); 
       assert_eq!(num, 10);
}

更改后,案例03正常执行。

 

以可变引用的方式捕获环境变量

如下代码:

// 变量捕获案例04
#[test]
fn test_closer_modify_env_var() {
        let mut num = 5;

        let mut add_num = |x: i32| {
            num += x;
            num
        };

        assert_eq!(add_num(1), 6);
        assert_eq!(add_num(1), 7);
        assert_eq!(num, 7);
}

以可变引用的方式捕获环境中的变量,与上一节讲的不可变引用捕获类似,差异在于:

(1)以可变引用捕获的变量,在闭包内可以修改该变量的值;

(2)以可变引用方式捕获变量的闭包,其绑定变量本身需要设置为mut;这是因为,对于闭包变量本身而言(add_num),在调用过程中,其所依赖的环境发生了变更(闭包 = 逻辑+环境, 环境变更被视为闭包内容变更)。

 

以获取所有权的方式捕获环境变量

见以下测试代码:

// 变量捕获案例05
#[test]
fn test_closer_capture_env_var() {
        let mut num = 5;
        let add_num = move |x: i32| {
            let mut inner = num;
            inner += x;
            inner
        };

        assert_eq!(add_num(1), 6);   // line-54:call once
        assert_eq!(add_num(1), 6);   // line-55:call again
        assert_eq!(num, 5);

        num = 10;
        assert_eq!(num, 10);
        assert_eq!(add_num(1), 6);  // line-60: call after change
}

与不可变引用、可变引用等捕获方式不同,获取所有权捕获的方式,会获取环境变量中的所有权!

当然,获取所有权的方式(即便关键字是move),遵循rust正常的变量绑定语义。在案例5中,i32类型实现了Copy,所以闭包中inner取得一个5的拷贝的所有权。如果,捕获的变量未实现Copy,则其所有权被闭包变量所持有,而原环境中的变量将不可用。示例如下:

// 变量捕获案例06
 #[test]
fn test_closer_capture_env_var2() {

        let prompt = "Hello, ".to_string();
        let call_some_one = move |s| {
            let mut inner = prompt;
            inner.push_str(s);
            inner
        };

        assert_eq!(call_some_one("Mir Wang"), "Hello, Mir Wang");
        assert_eq!(prompt, "Hello, ");
}

该代码编译运行会报错,原因是prompt被move到闭包内内部,原作用域内已经不可用了:

对比案例5与案例4,以可变引用捕获环境变量,在闭包中可以修改环境变量,该修改可以直接反馈到环境中;以捕获变量的方式,捕获发生在闭包创建的那一刻,捕获后保持不变(理解案例05中, line-54、line-55、line-60三次调用add_num返回值一致);并且,以捕获的方式捕获变量后,调用闭包,环境的值保持不变(对闭包无影响),因此闭包绑定的变量也无需加mut

闭包的实现与三种Fn trait

Rust 的闭包实现与其它语言有些许不同。它们实际上是trait的语法糖。

首先,Rust为闭包描述了三种模式,并将其定义trait:

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;
}

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 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;
}

细心的读者会注意到这些 trait 之间的些许区别,不过一个大的区别是selfFn获取&selfFnMut获取&mut self,而FnOnce获取self。关键的是,这个self是什么结构?我们在rust使用过程中,并未发行其定义!

要解释这一点,我们先要理解:在Rust中,object实现了某个trait,那么可以将这类object当做trait-object 使用(如果这点不理解,需要看一下rust trait 和trait object章节)。

而上面所述的,在Fn trait 中的这个self,就是每个特定闭包的临时结构体!(注意:这里“临时”这个描述不准确,因为我没有阅读过rust源码,不清楚rust在这里的处理细节,但我们可以这样简单的理解)

可以理解为:|...| {...}是 Fn trait的语法糖,编译器在处理这样的语句时,会做以下几个事情:

(1) 生成一个特定的结构体 obj,用于记录该闭包对环境变量的捕获情况;

(2)根据闭包表达式块中对环境变量的捕获方式,决定为该obj 实现 Fn 或者FnMut 或者FnOnce,并实现之;

(3)生成该结构体的实例,进行变量绑定。

 

其中,根据闭包表达式对环境变量的捕获方式,决定为该闭包实现的FnX trait的决策方式:

(1)如果闭包未捕获任何环境变量,或者只以不可变借用的方式捕获环境变量,那么为该闭包实现Fn ;

(2)如果闭包以可变引用的方式捕获环境变量,那么为该闭包实现FnMut;

(3)如果闭包已获取所有权的方式捕获环境变量,那么为该闭包实现FnOnce。

 

进一步,FnX trait 的一些特性也简要说明如下:

Fn 是一个能以不可变引用方式捕获环境变量的类函数调用接口。其特征包括(翻译自Fn trait 源码注释):

(1) Fn类型的实例可以被多次调用,而不改变状态。

(2)当闭包仅捕获不可变引用的变量或不捕获任何变量时,Fn会自动实现。

(3)一般地,安全的函数指针(有一些限制,详情请参考其文档)也会自动实现Fn。

(4)如果类型F实现了Fn,那么引用类型&F也实现了Fn。

(5)由于FnMut和FnOnce都是Fn的父特性,所以任何Fn实例都可以作为申明为FnMut或FnOnce参数的函数参数。

 

FnMut 是一个能以可变借用方式捕获环境变量的类函数调用特性。其特征包括:

(1) FnMut类型的实例可以被多次调用,并且可能改变状态。

(2) 当闭包捕获可变引用的变量时,FnMut会自动实现。

(3)所有实现Fn的类型(例如,安全的函数指针,因为FnMut是Fn的超特性)也会自动实现FnMut。

(4)对于任何实现FnMut的类型F,&mut F也实现了FnMut。

(5)由于FnOnce是FnMut的超特性,任何FnMut实例都可以用在期望FnOnce参数的地方。

 

FnOnce 是一个能够以获取所有权方式捕获环境变量的类函数调用特性。其特征包括:

(1) FnOnce类型的实例可以被调用,但可能无法多次调用。因此,如果只知道类型实现了FnOnce,那么它只能被调用一次。

(2)当闭包可能会消耗捕获的变量时,FnOnce会自动实现,以及所有实现FnMut的类型(例如,安全的函数指针,因为FnOnce是FnMut的超特性)。

(3)由于Fn和FnMut都是FnOnce的子特性,任何Fn或FnMut实例都可以用在期望FnOnce参数的地方。

 

总结而言,区分使用这三种Fn trait的方法为:

(1)当需要接受类似函数的参数类型,且只需要调用一次时,可以使用FnOnce作为约束。

(2)如果需要多次调用,使用FnMut作为约束;如果还需要保持无状态,使用Fn作为约束。

 

另外,需要再明确一点,Fnxx trait 是规则,而闭包是该规则的一些特殊实现;前面在在Fn trait特性说中,也提到了,一些安全的函数指针,也会实现Fn 或者FnMut;因此我们经常看到在一些接口中,入参采用Fn 限制,但实际使用时既可以传一般函数,也可以传闭包,其原因就在于此!

至此,闭包和Fn trait 的关系应该算是理清楚了!

 

下课之前,看一点代码巩固一下(编程人,好像脱离的代码总感觉空落落的)


代码示意

首先,定义了一些warpper 函数:

fn exec<'a, F: Fn(&'a str)>(mut f: F) {
    f("world")
}

fn exec_mut<'a, F: FnMut(&'a str)>(mut f: F) {
    f("world")
}

fn exec_once<'a, F: FnOnce(&'a str)>(f: F) {
    f("world");
}

测试 自动实现Fn的闭包可以用于 FnMut 及 FnOnce:

#[test]
fn test_exec() {
        let str = "hello".to_string();
        exec(|s| {
            let join = format!("{str} {s}");
            println!("{join}");
            assert_eq!(join.len(), 11);
        });

        let str2 = "hello".to_string();
        let closer_use = |s| {
            let join = format!("{str2} {s}");
            println!("{join}");
        };
        closer_use(" world");

        exec(closer_use);
        exec_mut(closer_use);
        exec_once(closer_use);
}

上述代码,closer_use 以不可变借用的方式借用str2, 该闭包实现Fn,因此可以用于exec函数接口;同时,Fn 自动实现FnMut及FnOnce,因此也可以用于exec_mut及exec_once!

 

测试自动实现FnMut的闭包用于 Fn 及 FnOnce

fn test_exec_mut() {
        let mut str = "hello ".to_string();
        exec_mut( |s| {
            str.push_str(s);
            println!("{str}")
        });

        let mut str2 = "hello ".to_string();
        let mut closer_mut = |s| {
            str.push_str(s);
            println!("{str}")
        };

        exec(closer_mut);
        exec_mut(closer_mut);
        exec_once(closer_mut);
}

编译运行该代码报错:

错误提示,当closer_mut 应用于exec函数时,提示特性不匹配,要求是Fn,但实际上只实现了FnMut,这个也符合预期!

其他的演示就不贴上来了,感兴趣的童鞋自己试一下吧......

 

0条评论
0 / 1000
huskar
18文章数
2粉丝数
huskar
18 文章 | 2 粉丝
原创

Rust入门(八) —— 理解闭包

2024-05-23 07:35:39
106
0

写(无)在(关)前(紧)面(要)的话

原计划会先写完Rust宏编程,但最近工作实在抽不出手,而且过程宏需要有一些编译理论支撑(比如抽象语法树), 还没有想好怎么用比较简单易懂的方式把这个讲清楚(主要是自己还没理清楚)。

最近在改一些项目工程,在实际项目中,闭包和迭代使用也是极其频繁;并且,在rust函数式链式调用过程中,配合闭包的使用,可以写出非常简洁的代码,艺术成分很高(恐怕有三四楼那么高啦)。另一方面,在工程中,去阅读别人写的这样的代码时,如果对这块内容掌握不太熟练,难免会觉得晦涩,比如下面这段代码:

如截图的函数,对入参engin_state进行一些列链式处理,中间掺杂了三次闭包用于数据处理和映射转换;看是一脉相承,但,于数据而言,却是“轻舟已过万重山”!

如果尝试去理解每一个转变的细节(当需要去理解数据处理细节、调整之前的数据转换模式时)就必须熟练掌握 Option工具套件、Result工具套件、闭包处理原理、迭代工具套件。

本着学以致用的目的,我们就随遇而安吧;遇都遇上了,就先整闭包吧,宏编程下集就下期再补......

闭包的应用场景

(1)封装私有变量和方法

通过闭包,可以创建私有变量和函数,只需要暴露必要的接口给外包,从而实现信息隐藏和封装。最经典的就是迭代器,迭代器通过暴露少量接口(iter,next 等)来支持数据循环获取,但隐藏了内部数据存储、获取、迭代的实现细节。

(2)函数式编程

闭包在函数式编程中是非常有用的工具,可以用于创建数据过滤、数据映射、高阶函数、延迟函数等。

(3)作为入参或回调函数

闭包可以作为入参传递到函数内部,在合适的时机进行执行;属于标准的注册、回调模型。这有利于实现异步处理中,状态存储和关联(相比于一般函数)。比如,在事件处理的回调函数中传递闭包,该闭包可能会携带事件初始化时的一些环境信息,这有助于提高事件处理过程中的信息完备性。

(4)数据缓存

利用闭包可以缓存计算结果,避免重复计算,从而提高性能。

 

总的来说,闭包在Rust中有着广泛的应用,能够帮助开发人员编写更加模块化、灵活和高效的代码(这句话感觉像是AI生成的呢)。

闭包的语法

一个典型的闭包,如下面示例:

let inc= |x:i32| {
    let mut result: i32 = x;
    result += 1;
    result
};

一个闭包通常包括两部分:其一,由“||”所包裹的参数描述;其二,有“{}”所包裹的表达式区域。如下:

| 【参数区域】| {【表达式区域】}

参数区域:描述0个或多个闭包函数的参数;参数描述与函数参数描述类似, 【参数名称】:【参数类型】,如果多个参数用 分割。

表达式区域:描述闭包的逻辑实现,其中最后一个表达式的值作为闭包的返回值。

注意:

(1)在书写闭包时,通常会省略闭包参数的类型。这是因为闭包通常运行在函数内部,存在上下文,rust编辑器可以进行类型推导。值得强调的是,闭包的参数(返回值)推导第一次完成后就固定了,不可变更,属于编译时的行为,而非运行时的行为。例如:

let self_x= |x| x;

let s = self_x(String::from("hello"));
let n = self_x(5);

上述代码,编译运行时报错:在编译self_x(5) 提示类型不匹配(期望是String,而传入i32)。原因是该闭包在 上一句编译时就完成类型推导:闭包的入参和返回都是String类型。

(2)如果表达式内容本身为一句话表达式,则可以省略“{}”。例如 上面示例中的self_x。另外,本节开始的inc闭包可以简化为:

let inc = |x| x+1;

捕获变量

闭包与函数的另一个重要的差异在于,闭包可以捕获其环境中的值(变量)。之所以把它称为“闭包”是因为它们“包含在环境中”(close over their environment)。

例如:

// 变量捕获案例01 
#[test]
fn test_closer_ref_use_env_var() {
        let mut num = 5;
        let num_plus = |x: i32| {
            num + x                        // 这里使用了外部的num变量
        };
        assert_eq!(num_plus(1), 6);
}

上述代码可以通过测试,num_plus捕获并使用环境中的num的值,与参数x运算后返回。

对应于rust函数参数传递的:引用传递,可变引用传递,值传递(移交所有权)三种模式,闭包在捕获环境中的变量时也有三种模式:

(1)不可变引用方式捕获

(2)可变引用方式捕获

(3)所有权方式捕获

接下来,我们逐一探讨。

以不可变引用的方式捕获环境变量

对案例01代码改进,尝试在闭包存活期间,修改环境中的变量num:

// 变量捕获案例02
#[test]
fn test_closer_ref_use_env_var() {
        let mut num = 5;

        let num_plus = |x: i32| {
            num + x
        };
        assert_eq!(num_plus(1), 6);
        assert_eq!(num, 5);

        num = 10;
        assert_eq!(num_plus(1), 11);
}

乍一看,上面的代码没有问题,但编译运行时会报错:

原因是: 在闭包中,num被借用(不可变借用),而在次期间尝试对num进行修改,进而违反了Rust的借用检测要求。

注意:当变量被闭包借用后,其引用关系会保持在闭包函数变量的生命周期内(案例中num_plus),在一些场景中,会导致引用被意外持有而影响数据更新!

案例02中, 对num进行后续赋值会报错,原因是num_plus的生命周期在此刻还未结束,删除最后一行(解除对num_plus引用,使得num_plus在num=10前结束生命周期,解除对num的引用),则num变得可以修改

// 变量捕获案例03
#[test]
fn test_closer_ref_use_env_var() {
        let mut num = 5;

        let num_plus = |x: i32| {
            num + x
        };
        assert_eq!(num_plus(1), 6);
        assert_eq!(num, 5);

        num = 10;
        // assert_eq!(num_plus(1), 11); 
       assert_eq!(num, 10);
}

更改后,案例03正常执行。

 

以可变引用的方式捕获环境变量

如下代码:

// 变量捕获案例04
#[test]
fn test_closer_modify_env_var() {
        let mut num = 5;

        let mut add_num = |x: i32| {
            num += x;
            num
        };

        assert_eq!(add_num(1), 6);
        assert_eq!(add_num(1), 7);
        assert_eq!(num, 7);
}

以可变引用的方式捕获环境中的变量,与上一节讲的不可变引用捕获类似,差异在于:

(1)以可变引用捕获的变量,在闭包内可以修改该变量的值;

(2)以可变引用方式捕获变量的闭包,其绑定变量本身需要设置为mut;这是因为,对于闭包变量本身而言(add_num),在调用过程中,其所依赖的环境发生了变更(闭包 = 逻辑+环境, 环境变更被视为闭包内容变更)。

 

以获取所有权的方式捕获环境变量

见以下测试代码:

// 变量捕获案例05
#[test]
fn test_closer_capture_env_var() {
        let mut num = 5;
        let add_num = move |x: i32| {
            let mut inner = num;
            inner += x;
            inner
        };

        assert_eq!(add_num(1), 6);   // line-54:call once
        assert_eq!(add_num(1), 6);   // line-55:call again
        assert_eq!(num, 5);

        num = 10;
        assert_eq!(num, 10);
        assert_eq!(add_num(1), 6);  // line-60: call after change
}

与不可变引用、可变引用等捕获方式不同,获取所有权捕获的方式,会获取环境变量中的所有权!

当然,获取所有权的方式(即便关键字是move),遵循rust正常的变量绑定语义。在案例5中,i32类型实现了Copy,所以闭包中inner取得一个5的拷贝的所有权。如果,捕获的变量未实现Copy,则其所有权被闭包变量所持有,而原环境中的变量将不可用。示例如下:

// 变量捕获案例06
 #[test]
fn test_closer_capture_env_var2() {

        let prompt = "Hello, ".to_string();
        let call_some_one = move |s| {
            let mut inner = prompt;
            inner.push_str(s);
            inner
        };

        assert_eq!(call_some_one("Mir Wang"), "Hello, Mir Wang");
        assert_eq!(prompt, "Hello, ");
}

该代码编译运行会报错,原因是prompt被move到闭包内内部,原作用域内已经不可用了:

对比案例5与案例4,以可变引用捕获环境变量,在闭包中可以修改环境变量,该修改可以直接反馈到环境中;以捕获变量的方式,捕获发生在闭包创建的那一刻,捕获后保持不变(理解案例05中, line-54、line-55、line-60三次调用add_num返回值一致);并且,以捕获的方式捕获变量后,调用闭包,环境的值保持不变(对闭包无影响),因此闭包绑定的变量也无需加mut

闭包的实现与三种Fn trait

Rust 的闭包实现与其它语言有些许不同。它们实际上是trait的语法糖。

首先,Rust为闭包描述了三种模式,并将其定义trait:

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;
}

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 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;
}

细心的读者会注意到这些 trait 之间的些许区别,不过一个大的区别是selfFn获取&selfFnMut获取&mut self,而FnOnce获取self。关键的是,这个self是什么结构?我们在rust使用过程中,并未发行其定义!

要解释这一点,我们先要理解:在Rust中,object实现了某个trait,那么可以将这类object当做trait-object 使用(如果这点不理解,需要看一下rust trait 和trait object章节)。

而上面所述的,在Fn trait 中的这个self,就是每个特定闭包的临时结构体!(注意:这里“临时”这个描述不准确,因为我没有阅读过rust源码,不清楚rust在这里的处理细节,但我们可以这样简单的理解)

可以理解为:|...| {...}是 Fn trait的语法糖,编译器在处理这样的语句时,会做以下几个事情:

(1) 生成一个特定的结构体 obj,用于记录该闭包对环境变量的捕获情况;

(2)根据闭包表达式块中对环境变量的捕获方式,决定为该obj 实现 Fn 或者FnMut 或者FnOnce,并实现之;

(3)生成该结构体的实例,进行变量绑定。

 

其中,根据闭包表达式对环境变量的捕获方式,决定为该闭包实现的FnX trait的决策方式:

(1)如果闭包未捕获任何环境变量,或者只以不可变借用的方式捕获环境变量,那么为该闭包实现Fn ;

(2)如果闭包以可变引用的方式捕获环境变量,那么为该闭包实现FnMut;

(3)如果闭包已获取所有权的方式捕获环境变量,那么为该闭包实现FnOnce。

 

进一步,FnX trait 的一些特性也简要说明如下:

Fn 是一个能以不可变引用方式捕获环境变量的类函数调用接口。其特征包括(翻译自Fn trait 源码注释):

(1) Fn类型的实例可以被多次调用,而不改变状态。

(2)当闭包仅捕获不可变引用的变量或不捕获任何变量时,Fn会自动实现。

(3)一般地,安全的函数指针(有一些限制,详情请参考其文档)也会自动实现Fn。

(4)如果类型F实现了Fn,那么引用类型&F也实现了Fn。

(5)由于FnMut和FnOnce都是Fn的父特性,所以任何Fn实例都可以作为申明为FnMut或FnOnce参数的函数参数。

 

FnMut 是一个能以可变借用方式捕获环境变量的类函数调用特性。其特征包括:

(1) FnMut类型的实例可以被多次调用,并且可能改变状态。

(2) 当闭包捕获可变引用的变量时,FnMut会自动实现。

(3)所有实现Fn的类型(例如,安全的函数指针,因为FnMut是Fn的超特性)也会自动实现FnMut。

(4)对于任何实现FnMut的类型F,&mut F也实现了FnMut。

(5)由于FnOnce是FnMut的超特性,任何FnMut实例都可以用在期望FnOnce参数的地方。

 

FnOnce 是一个能够以获取所有权方式捕获环境变量的类函数调用特性。其特征包括:

(1) FnOnce类型的实例可以被调用,但可能无法多次调用。因此,如果只知道类型实现了FnOnce,那么它只能被调用一次。

(2)当闭包可能会消耗捕获的变量时,FnOnce会自动实现,以及所有实现FnMut的类型(例如,安全的函数指针,因为FnOnce是FnMut的超特性)。

(3)由于Fn和FnMut都是FnOnce的子特性,任何Fn或FnMut实例都可以用在期望FnOnce参数的地方。

 

总结而言,区分使用这三种Fn trait的方法为:

(1)当需要接受类似函数的参数类型,且只需要调用一次时,可以使用FnOnce作为约束。

(2)如果需要多次调用,使用FnMut作为约束;如果还需要保持无状态,使用Fn作为约束。

 

另外,需要再明确一点,Fnxx trait 是规则,而闭包是该规则的一些特殊实现;前面在在Fn trait特性说中,也提到了,一些安全的函数指针,也会实现Fn 或者FnMut;因此我们经常看到在一些接口中,入参采用Fn 限制,但实际使用时既可以传一般函数,也可以传闭包,其原因就在于此!

至此,闭包和Fn trait 的关系应该算是理清楚了!

 

下课之前,看一点代码巩固一下(编程人,好像脱离的代码总感觉空落落的)


代码示意

首先,定义了一些warpper 函数:

fn exec<'a, F: Fn(&'a str)>(mut f: F) {
    f("world")
}

fn exec_mut<'a, F: FnMut(&'a str)>(mut f: F) {
    f("world")
}

fn exec_once<'a, F: FnOnce(&'a str)>(f: F) {
    f("world");
}

测试 自动实现Fn的闭包可以用于 FnMut 及 FnOnce:

#[test]
fn test_exec() {
        let str = "hello".to_string();
        exec(|s| {
            let join = format!("{str} {s}");
            println!("{join}");
            assert_eq!(join.len(), 11);
        });

        let str2 = "hello".to_string();
        let closer_use = |s| {
            let join = format!("{str2} {s}");
            println!("{join}");
        };
        closer_use(" world");

        exec(closer_use);
        exec_mut(closer_use);
        exec_once(closer_use);
}

上述代码,closer_use 以不可变借用的方式借用str2, 该闭包实现Fn,因此可以用于exec函数接口;同时,Fn 自动实现FnMut及FnOnce,因此也可以用于exec_mut及exec_once!

 

测试自动实现FnMut的闭包用于 Fn 及 FnOnce

fn test_exec_mut() {
        let mut str = "hello ".to_string();
        exec_mut( |s| {
            str.push_str(s);
            println!("{str}")
        });

        let mut str2 = "hello ".to_string();
        let mut closer_mut = |s| {
            str.push_str(s);
            println!("{str}")
        };

        exec(closer_mut);
        exec_mut(closer_mut);
        exec_once(closer_mut);
}

编译运行该代码报错:

错误提示,当closer_mut 应用于exec函数时,提示特性不匹配,要求是Fn,但实际上只实现了FnMut,这个也符合预期!

其他的演示就不贴上来了,感兴趣的童鞋自己试一下吧......

 

文章来自个人专栏
后台开发技术分享
18 文章 | 4 订阅
0条评论
0 / 1000
请输入你的评论
4
2