首页 文章

关于向量时移动闭包的关键字

提问于
浏览
4

有人告诉我,在闭包中使用 move 关键字时,闭包在其环境中获取变量副本的所有权 . 但是这段代码不会编译:

use std::thread;
use std::time::Duration;

fn main() {
    let mut data = vec![1, 2, 3];

    for i in 0..3 {
        thread::spawn(move || {
            data[i] += 1;
        });
    }

    thread::sleep(Duration::from_millis(50));
}

错误消息是

8:17 error: capture of moved value: `data`
    data[i] += 1;
    ^~~~

对此有一些解释

Rust知道这不安全!如果我们在每个线程中都有对数据的引用,并且该线程获得了引用的所有权,那么我们将拥有三个所有者!

这是否意味着副本只复制堆栈上的元数据而不是堆上的实际数据?换句话说,复制引用是因为数据类型本身本质上是引用类型,而不是引用 &mut 语法创建的引用 . 对于所有数据类型, move 关键字是否在堆栈上复制数据?因此,当它类型为 i32 时,它按值复制,如果类型为矢量则通过引用复制 .

我的初衷是了解 move 关键字的确切行为 . 仔细看看Rust文档后,我认为它遵循一般变量绑定的移动语义 . 在这种情况下,"data"的所有权只能转移一次 . 虽然将 0..3 更改为 0..1 确实没有复制堆栈上的元数据,而不是堆数据 .

3 回答

  • 0

    这是否意味着副本只复制堆栈上的元数据而不是堆上的实际数据?换句话说,复制引用是因为数据类型本身本质上是引用类型,而不是引用&mut语法创建的引用 . 对于所有数据类型,move关键字是否在堆栈上复制数据?因此,当它像i32这样的类型时,它会按值复制,如果它是类型为vector,则通过引用复制 .

    转移变量的所有权时,不执行深层复制 . 所有指针仍然指向相同的值,并且不触及堆内存 . 所有权转让是一种廉价的设计操作 . 如果需要深层副本,可以在 Vec 上显式调用 clone 方法 . 克隆价格低廉的某些类型(例如 i32 )实现了 Copy trait,这意味着如果您尝试将相同值的所有权传递给多个目标,则会自动调用 clone .

    我的初衷是了解move关键字的确切行为 . 仔细看看Rust文档后,我认为它遵循一般变量绑定的移动语义 . 在这种情况下,“数据”的所有权只能转移一次 . 虽然将0..3更改为0..1并没有什么区别 . 确实,堆栈上的元数据是复制的,而不是堆数据 .

    使用 0..1 doesn 't work because the compiler doesn' t检查可迭代范围 0..1 是否只包含单个元素 . 所以使用 0..1 不能编译,但完全删除 for 循环在逻辑上是等效的并且编译

    use std::thread;
    use std::time::Duration;
    
    fn main() {
        let mut data = vec![1, 2, 3];
    
        thread::spawn(move || {
            data[0] += 1;
        });
    
        thread::sleep(Duration::from_millis(50));
    }
    

    如果我们在将它传递给 thread::spawn 后再次尝试访问 data ,我们将收到类似 error: use of moved value: 'data' 的编译错误 . 这就是 move 关键字的作用 . 由于 data 已被移至闭包,该闭包现在负责释放其内存 . 值得注意的是,如果没有 move 关键字,此代码将无法编译,因为在这种情况下 data 将在 main 函数的末尾被释放,但该线程可能比 main 函数更长 .

  • 2

    move 的实现可能是 memcpy 或无操作,具体取决于编译器优化 . 这里编译器检查语言级别而不是实现 .

    这里发生错误是因为你试图移动 data 三次 .

    你做了什么

    thread::spawn(move || {
        data[i] += 1;
    });
    

    是将 data 移动到新生成的线程,并递增 i th值 . 你通过产生三个线程来做这三次 .

    如果要更改原始向量中的值,则需要使用引用,并确保引用在这些新线程的整个范围内都有效 . std::thread::spawn 不能那样做,你可能要看crossbeam::Scope .

    对于实现, move 通常不需要深层复制,并且它通常不用于避免昂贵的堆分配 .

  • 1

    “通过引用”是一个被抛出的术语......并且它很少在任何语言中正确使用 . 移动矢量时,将按“值”复制 . 矢量由一些东西组成(可能不是100%准确但通常是针对这样的结构......或Go中的切片):

    • 指向堆数据的指针 .

    • 一个长度 .

    • 容量 .

    这不会改变堆分配的数据..现在只有另一个指针指向它 . 复制这些数据非常便宜 - 它只有3个整数 .

    TLDR是:您的假设是正确的 . 无论什么被确定为“参考”通常是在“按值传递”环境中复制的内容 .

    编辑:我删除了与C#引用无用的比较..但短版本是:C#是“按值传递”,因此将引用类型传递给方法不是“通过引用传递” - 复制引用 .

相关问题