likes
comments
collection
share

【Rust 进阶教程】 01 闭包与所有权

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

0x00 开篇

从本篇文章开始,我们进入 Rust 进阶教程 的学习,在这个模块中有新知识也有旧知识。对于就知识,我们会对它作更深入的理解。对于新知识,有关并发编程、模块化编程等等也都将一起介绍。

0x01 Fn、FnMut、FnOnce

前面已经介绍过闭包的基本概念了,那你有没有考虑过被闭包捕获变量的类型是引用还是所有权吗?

fn main() {
    let hello = "hello rust".to_string();
    let c = || {
		// hello 在这里是什么呢?
        println!("{}", hello);
    };
    c();
    println!("{}", hello);
}

首先我们先来了解下这三个 trait Fn、FnMut、FnOnce 的概念。这三个trait 的区别在于它们是以哪种方式来捕获外部的变量。

Fn : 可以被多次调用。这种闭包不能改变捕获变量的值,可以使用 & 来捕获变量的引用。对于 Copy 类型,则默认会以不可变引用的方式来捕获它。

fn main() {
	let x = 10;
    let c1 = || {
        println!("{}", x);
    };
    c1();
    c1();
    println!("{}", x);
}
// 运行结果
// 10
// 10
// 10

FnMut : 可以被多次调用。这种闭包可以改变捕获变量的值。

fn main() {
	let mut y = 10;
    let mut c2 = || {
        y += 10;
    };
    c2();
    c2();
    println!("{}", y);
}
// 运行结果
// 30

FnOnce : 只能被调用一次。这种闭包在调用后会将捕获变量的所有权移动到闭包内部,可以使用 move 关键字来捕获变量的所有权。因为闭包会移动变量的所有权,所以在使用完后就不能再次访问这些变量了。

fn main() {
	let z = "rust".to_string();
    let c3 = move || {
        println!("{}", z);
    };
    c3();
    // 无法再次调用
    // c3();
    // 无法再次使用 z
    // println!("{}", z);
}

0x02 解读源码 Fn、FnMut、FnOnce

Fn 、FnMut 、FnOnce 是三个 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;
}

我们可以看到,他们之间的关系是继承关系。

【Rust 进阶教程】 01 闭包与所有权

上面我们也只是通过概念来判断它们属于哪种类型,接下来我们用代码实际验证下:

我们创建 3 个函数(如下),如果编译通过,说明它的类型是没有问题的。

fn is_Fn<F>(_: &F) where F: Fn() -> () {}

fn is_FnMut<F>(_: &F) where F: FnMut() -> () {}

fn is_FnOnce<F>(_: &F) where F: FnOnce() -> () {}

实现代码如下:

fn main() {
	let x = 5;
    let c1 = || {
        println!("{}", x);
    };

    let mut y = 6;
    let mut c2 = || {
        y += 1;
        println!("{}", y);
    };

    let z = "rust".to_string();
    let c3 = move || {
        println!("{}", z);
    };

    
    is_Fn(&c1);
    is_FnMut(&c1);
    is_FnOnce(&c1);

    is_FnMut(&c2);
    is_FnOnce(&c2);

    is_FnOnce(&c3);
}
// 运行结果
// 编译通过

由于Fn 、FnMut 、FnOnce 之间的继承关系。这就表明,c1 可以正常调用这三个函数,c2 可以调用 is_FnMut 和 is_FnOncec3 只能调用 is_FnOnce

0x03 闭包到底是什么?(扩展阅读)

说了这么多,那么闭包导致是什么呢?我们也从源码中找到了些许答案。在 rustc_middle/sty.rs 中解释道:通常一个闭包可以被建模为一个结构体。

// rustc_middle/sty.rs
struct Closure<'l0...'li, T0...Tj, CK, CS, U>(...U);

'l0...'li 表示这个结构体具有 'l0 到 'li 这些生命周期参数。

T0...Tj 表示这个结构体还有 T0 到 Tj 这些类型参数。

CK 表示闭包的种类,是 ClousreKind的缩写。有 Fn 、 FnMut 、FnOnce 三种。定义如下:

// rustc_middle/ty/closure.rs
pub enum ClosureKind {
    // 这里也提示顺序很重要,Fn 是 FnMut 的 subtrait。Fn < FnMut < FnOnce。
    Fn,
    FnMut,
    FnOnce,
}

CS 表示闭包的签名,是 Closure Signatures 的缩写,类似于函数的签名,看做是 fn() 类型。例如,fn(u32, u32) -> u32 意味着闭包实现了CK<(u32, u32), Output = u32>

U 可访问的参数的类型,在编程语言中通常简写为 upvar 是 upward-exposed variable 的缩写。表示在外部作用域中声明并在内部作用域中引用的变量。

那假设现在有一个闭包 c

let mut x: i32 = 5;

let mut c = || {
    x += 5;
    x
}
c();

那我们就可以将其视作一个闭包结构体类型,那上面的:

struct Closure<'a> {
   x: i32
}

impl<'a> FnMut<()> for Closure<'a> {
    type Output = i32;
    fn call_mut(self) -> i32 {
        self.s += 5;
        self.s
    }
}
// 最终的闭包调用形式类似
Closure {x: 5};

由于篇幅的问题,我这里也仅仅是浅浅的介绍了下闭包的定义。如果你想更深入的了解,可以自己看下源码。

0x04 小结

关于闭包,我们主要要分清使用的是哪种闭包,以及不同闭包的区别。最后再总结下吧:

  • Copy 类型: 当捕获变量时以不可变引用捕获。
  • 非 Copy 类型: 捕获变量时,可以通过不可变引用来捕获,也可以移动该值的所有权。

另外可变绑定类型,如果在闭包内需求对其进行修改操作,则需要使用可变 &mut T 来捕获。

转载自:https://juejin.cn/post/7227296961358856249
评论
请登录