zoukankan      html  css  js  c++  java
  • 【译】理解Rust中的闭包

    原文标题:Understanding Closures in Rust
    原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f286ed1759
    公众号: Rust 碎碎念
    翻译 by: Praying

    概要

    • 闭包(closure)是函数指针(function pointer)和上下文(context)的组合。
    • 没有上下文的闭包就是一个函数指针。
    • 带有不可变上下文(immutable context)的闭包属于Fn
    • 带有可变上下文(mutable context)的闭包属于FnMut
    • 拥有其上下文的闭包属于FnOnce

    理解 Rust 中不同类型的闭包

    不同于其他语言,Rust 对self参数的使用是显式的。当我们实现结构体时,必须把self作为函数签名的第一个参数:

    struct MyStruct {
        text: &'static str,
        number: u32,
    }

    impl MyStruct {
        fn new (text: &'static str, number: u32) -> MyStruct {
            MyStruct {
                text: text,
                number: number,
            }
        }

        // We have to specify that 'self' is an argument.
        fn get_number (&self) -> u32 {
            self.number
        }

        // We can specify different kinds of ownership and mutability of self.
        fn inc_number (&mut self) {
            self.number += 1;
        }

        // There are three different types of 'self'
        fn destructor (self) {
            println!("Destructing {}"self.text);
        }
    }

    因此,下面这两种风格是一样的:

    obj.get_number();
    MyStruct::get_number(&obj);

    这和那些把self(或this)隐藏起来的语言不同。在那些语言中,只要将一个函数和一个对象或结构关联起来,就隐含着第一个参数是self。在上面的代码中,我们有四个self选项:一个不可变引用,一个可变引用,一个被拥有的值,或者压根就没有使用self作为参数。

    因此,self表示函数执行的某一类上下文。它在 Rust 中是显式的,但是在其他语言中经常是隐含的。

    此外,在本文中,我们将会使用下面的函数:

    fn is_fn <A, R>(_x: fn(A) -> R) {}
    fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
    fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
    fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}

    这些函数的唯一作用是类型检查。例如,如果is_FnMut(&func)能够编译,那么我们就可以知道那个func属于FnMut trait。

    无上下文和fn(小写的 f)类型

    鉴于上述内容,考虑几个使用(上面定义的)MyStruct的闭包的例子:

    let obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    let closure1 = |x: &MyStruct| x.get_number() + 3;
    assert_eq!(closure1(&obj1), 18);
    assert_eq!(closure1(&obj2), 13);

    这是我们可以得到的最简单的(代码示例)。这个闭包为类型MyStruct的任意对象中的已有数字加上三。它可以在任意位置被执行,不会有任何问题,并且编译器不会给你带来任何麻烦。我们可以很简单地写出下面这样的代码替代closure1:

    // It doesn't matter what code appears here, the function will behave
    // exactly the same.
    fn func1 (x: &MyStruct) -> u32 {
        x.get_number() + 3
    }
    assert_eq!(func1(&obj1), 18);
    assert_eq!(func1(&obj2), 13);

    这个函数不依赖于它的上下文。无论它之前和之后发生了什么,它的行为都是一致的。我们(几乎)可以互换着使用func1closure1

    当一个闭包完全不依赖上下文时,我们的闭包的类型就是fn

    // compiles successfully.
    is_fn(closure1);
    is_Fn(&closure1);
    is_FnMut(&closure1);
    is_FnOnce(&closure1);

    可变上下文和Fn(大写的 F)trait

    相较于上面,我们可以为闭包添加一个上下文。

    let obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    // obj1 is borrowed by the closure immutably.
    let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
    assert_eq!(closure2(&obj2), 25);
    // We can borrow obj1 again immutably...
    assert_eq!(obj1.get_number(), 15);
    // But we can't borrow it mutably.
    // obj1.inc_number();               // ERROR

    closure2依赖于obj1的值,并且包含周围作用域的信息。在这段代码中,closure2将会借用obj1从而使得它可以在函数体中使用obj1。我们还可以对obj1进行不可变借用,但是如果我们试图在后面修改obj1,我们将会得到一个借用错误。

    如果我们尝试使用fn语法来重写我们的闭包,我们在函数内部需要知道的一切都必须作为参数传递给函数,所以我们添加了一个额外的参数来表示函数的上下文:

    struct Context<'a>(&'a MyStruct);
    let obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    let ctx = Context(&obj1);
    fn func2 (context: &Context, x: &MyStruct) -> u32 {
        x.get_number() + context.0.get_number()
    }

    其行为和我们的闭包基本一致:

    assert_eq!(func2(&ctx, &obj2), 25);
    // We can borrow obj1 again immutably...
    assert_eq!(obj1.get_number(), 15);
    // But we can't borrow it mutably.
    // obj1.inc_number(); // ERROR

    注意,Context结构体包含一个对MyStruct结构体的不可变引用,这表明我们将无法在函数内部修改它。

    当我们调用closure1时,也意味着我们我们把周围的上下文作为参数传递给了closure1,正如我们在使用fn时必须要做的那样。在一些其他的语言中,我们不必指定将self作为参数传递,与之类似,Rust 也不需要我们显式地指定将上下文作为参数传递。

    当一个闭包以不可变引用的方式捕获上下文,我们称它实现了Fn trait。这表明我们可以不必修改上下文而多次调用我们的函数。

    // Does not compile:
    // is_fn(closure2);
    // Compiles successfully:
    is_Fn(&closure2);
    is_FnMut(&closure2);
    is_FnOnce(&closure2);

    可变上下文和FnMut trait

    如果我们在闭包内部修改obj1,我们会得到不同的结果:

    let mut obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    // obj1 is borrowed by the closure mutably.
    let mut closure3 = |x: &MyStruct| {
        obj1.inc_number();
        x.get_number() + obj1.get_number()
    };
    assert_eq!(closure3(&obj2), 26);
    assert_eq!(closure3(&obj2), 27);
    assert_eq!(closure3(&obj2), 28);
    // We can't borrow obj1 mutably or immutably
    // assert_eq!(obj1.get_number(), 18);   // ERROR
    // obj1.inc_number();                   // ERROR

    这一次我们不能对obj1进行可变借用和不可变借用了。我们还必须得把闭包标注为mut。如果我们希望使用fn语法重写这个函数,将会得到下面的代码:

    struct Context<'a>(&'a mut MyStruct);
    let mut obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    let mut ctx = Context(&mut obj1);
    // obj1 is borrowed by the closure mutably.
    fn func3 (context: &mut Context, x: &MyStruct) -> u32 {
        context.0.inc_number();
        x.get_number() + context.0.get_number()
    };

    其行为与closure3相同:

    assert_eq!(func3(&mut ctx, &obj2), 26);
    assert_eq!(func3(&mut ctx, &obj2), 27);
    assert_eq!(func3(&mut ctx, &obj2), 28);
    // We can't borrow obj1 mutably or immutably
    // assert_eq!(obj1.get_number(), 18);       // ERROR
    // obj1.inc_number();                       // ERROR

    注意,我们必须把我们的上下文以可变引用的方式传递。这表明每次调用我们的函数后,可能会得到不同的结果。

    当闭包以可变引用捕获它的上下文时,我们称它属于FnMut trait。

    // Does not compile:
    // is_fn(closure3);
    // is_Fn(&closure3);
    // Compiles successfully:
    is_FnMut(&closure3);
    is_FnOnce(&closure3);

    拥有的上下文

    在我们的最后一个例子中,我们将会获取obj1的所有权:

    let obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    // obj1 is owned by the closure
    let closure4 = |x: &MyStruct| {
        obj1.destructor();
        x.get_number()
    };

    我们必须在使用closure4之前检查它的类型:

    // Does not compile:
    // is_fn(closure4);
    // is_Fn(&closure4);
    // is_FnMut(&closure4);
    // Compiles successfully:
    is_FnOnce(&closure4);

    现在我们可以检查它的行为:

    assert_eq!(closure4(&obj2), 10);
    // We can't call closure4 twice...
    // assert_eq!(closure4(&obj2), 10);             //ERROR
    // We can't borrow obj1 mutably or immutably
    // assert_eq!(obj1.get_number(), 15);           // ERROR
    // obj1.inc_number();                           // ERROR

    在这个例子中,我们只能调用这个函数一次。一旦我们对它进行了第一次调用,obj1就被我们销毁了, 所以它在第二次调用的时候也就不复存在了。Rust 会给我们一个关于使用一个已经被移动的值的错误。这就是为什么我们要事先检查其类型。

    使用fn语法来实现,我们可以得到下面的代码:

    struct Context(MyStruct);
    let obj1 = MyStruct::new("Hello"15);
    let obj2 = MyStruct::new("More Text"10);
    let ctx = Context(obj1);
    // obj1 is owned by the closure
    fn func4 (context: Context, x: &MyStruct) -> u32 {
        context.0.destructor();
        x.get_number()
    };

    这段代码,正如我们所预期的,和我们的闭包行为一致:

    assert_eq!(func4(ctx, &obj2), 10);
    // We can't call func4 twice...
    // assert_eq!(func4(ctx, &obj2), 10);             //ERROR
    // We can't borrow obj1 mutably or immutably
    // assert_eq!(obj1.get_number(), 15);           // ERROR
    // obj1.inc_number();                           // ERROR

    当我们使用fn来写我们的闭包时,我们必须使用一个Context结构体并拥有它的值。当一个闭包拥有其上下文的所有权时,我们称它实现了FnOnce。我们只能调用这个函数一次,因为在调用之后,上下文就被销毁了。

    总结

    • 不需要上下文的函数拥有fn类型,并且可以在任意位置调用。

    • 仅需要不可变地访问其上下文的函数属于Fn trait,并且只要上下文在作用域中存在,就可以在任意位置调用。

    • 需要可变地访问其上下文的函数实现了FnMut trait,可以在上下文有效的任意位置调用,但是每次调用可能会做不同的事情。

    • 获取上下文所有权的函数只能被调用一次。

  • 相关阅读:
    matlab中s函数编写心得-转自水木
    webgl编程指南笔记【一】
    node辅助工具npm、yarn、nrm、n、Nodemon
    es6复习笔记
    游戏客户端开发劝退
    一步步搭建现代前端框架(三)
    一步步搭建现代前端框架(二)
    一步步搭建现代前端框架(一)
    vue 生命周期钩子 路由钩子 动画钩子 执行顺序
    怎样提升手机相机照片效果
  • 原文地址:https://www.cnblogs.com/praying/p/13964923.html
Copyright © 2011-2022 走看看