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,可以在上下文有效的任意位置调用,但是每次调用可能会做不同的事情。

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

  • 相关阅读:
    图像检索(image retrieval)- 11
    图像检索(image retrieval)- 10相关
    Mock.js简易教程,脱离后端独立开发,实现增删改查功能
    Azure Monitor (3) 对虚拟机磁盘设置自定义监控
    Azure Monitor (1) 概述
    Azure SQL Managed Instance (2) 备份SQL MI
    Azure Virtual Network (17) Private Link演示
    Azure Virtual Network (16) Private Link
    Azure Virtual Network (15) Service Endpoint演示
    Azure Virtual Network (14) Service Endpoint服务终结点
  • 原文地址:https://www.cnblogs.com/praying/p/13964923.html
Copyright © 2011-2022 走看看