首页 文章

什么是Rust中的移动语义?

提问于
浏览
15

在Rust中,有两种可能性来引用

  • Borrow ,即参考但不允许改变参考目的地 . & 运算符从值中借用所有权 .

  • Borrow mutably ,即参考改变目的地 . &mut 运营商可能会从一个值中借用所有权 .

Rust documentation about borrowing rules说:

首先,任何借款必须持续不超过业主的范围 . 其次,您可能拥有这两种借用中的一种或另一种,但不能同时具有这两种:一个或多个引用(&T)到资源,恰好是一个可变引用(&mut T) .

我相信引用一个引用是创建一个指向值的指针并通过指针访问该值 . 如果有更简单的等效实现,编译器可以优化它 .

但是,我不明白 move 的含义以及它是如何实现的 .

对于实现 Copy 特征的类型,它意味着复制,例如通过从源分配结构成员,或 memcpy() . 对于小结构或原始数据,此副本是有效的 .

并为 move

这个问题不是What are move semantics?的重复,因为Rust和C是不同的语言,并且两者之间的移动语义不同 .

4 回答

  • 1

    Semantics

    Rust实现了所谓的Affine Type System

    仿射类型是强加较弱约束的线性类型的一种形式,对应于仿射逻辑 . 仿射资源只能使用一次,而线性资源只能使用一次 .

    Copy 并因此被移动的类型是仿射类型:您可以使用它们一次或从不使用它们 .

    Rust认为这是以其以所有权为中心的世界观(*)的所有权转移 .

    (*)一些在Rust工作的人比我在CS中更有资格,他们故意实施仿射型系统;然而,与公开math-y / cs-y概念的Haskell相反,Rust倾向于暴露出更实用的概念 .

    注意:可以认为从标记为 #[must_use] 的函数返回的仿射类型实际上是我阅读中的线性类型 .


    Implementation

    这取决于 . 请记住,Rust是一种为速度而构建的语言,这里有许多优化过程,这取决于所使用的编译器(在我们的例子中是rustc LLVM) .

    在函数体(playground)中:

    fn main() {
        let s = "Hello, World!".to_string();
        let t = s;
        println!("{}", t);
    }
    

    如果检查LLVM IR(在Debug中),您将看到:

    %_5 = alloca %"alloc::string::String", align 8
    %t = alloca %"alloc::string::String", align 8
    %s = alloca %"alloc::string::String", align 8
    
    %0 = bitcast %"alloc::string::String"* %s to i8*
    %1 = bitcast %"alloc::string::String"* %_5 to i8*
    call void @llvm.memcpy.p0i8.p0i8.i64(i8* %1, i8* %0, i64 24, i32 8, i1 false)
    %2 = bitcast %"alloc::string::String"* %_5 to i8*
    %3 = bitcast %"alloc::string::String"* %t to i8*
    call void @llvm.memcpy.p0i8.p0i8.i64(i8* %3, i8* %2, i64 24, i32 8, i1 false)
    

    在封面下方,rustc从 "Hello, World!".to_string()s 的结果调用 memcpy ,然后调用 t . 虽然它看起来效率低下,但在发布模式下检查相同的IR,您会发现LLVM已经完全省略了副本(意识到 s 未被使用) .

    调用函数时会出现同样的情况:理论上你将对象“移动”到函数堆栈框架中,但实际上如果对象很大,则rustc编译器可能会切换到传递指针 .

    另一种情况是从函数返回,但即使这样,编译器也可以应用"return value optimization"并直接在调用者的堆栈帧中构建 - 也就是说,调用者传递一个指针来写入返回值,该指针在没有中间存储的情况下使用 .

    Rust的所有权/借用限制允许在C中难以达到的优化(其中也有RVO,但在很多情况下不能应用它) .

    那么,摘要版本:

    • 移动大型物体效率低下,但有许多优化措施可能会完全忽略此举

    • 移动涉及 std::mem::size_of::<T>()std::mem::size_of::<T>() 字节,因此移动大 String 是有效的,因为它只有几个字节,无论它们保持的分配缓冲区的大小如何

  • 14

    移动项目时,您正在转移该项目的所有权 . 这是Rust的一个关键组成部分 .

    假设我有一个结构,然后我将结构从一个变量分配给另一个变量 . 默认情况下,这将是一个举动,我已转让所有权 . 编译器将跟踪此所有权更改并阻止我再使用旧变量:

    pub struct Foo {
        value: u8,
    }
    
    fn main() {
        let foo = Foo { value: 42 };
        let bar = foo;
    
        println!("{}", foo.value); // error: use of moved value: `foo.value`
        println!("{}", bar.value);
    }
    

    如何实施 .

    从概念上讲,移动某些东西不需要做任何事情 . 在上面的例子中,实际上并不知道编译器的作用,它可能会根据优化级别而改变 .

    但是,出于实际目的,您可以认为当您移动某些内容时,表示该项目的位将被复制,就像通过 memcpy 一样 . 这有助于解释当您将变量传递给使用它的函数时,或者从函数返回值时(再次,优化器可以执行其他操作以使其有效,这只是概念上)时会发生什么:

    // Ownership is transferred from the caller to the callee
    fn do_something_with_foo(foo: Foo) {} 
    
    // Ownership is transferred from the callee to the caller
    fn make_a_foo() -> Foo { Foo { value: 42 } }
    

    "But wait!",你说,“ memcpy 只在实现 Copy 的类型中发挥作用!” . 这大部分都是正确的,但最大的区别在于当一个类型实现 Copy 时,源和目标都可以在之后使用副本!

    一种思考移动语义的方法与复制语义相同,但附加的限制是移动的东西不再是要使用的有效项 .

    但是,通常更容易从另一个角度来考虑它:您可以做的最基本的事情是移动/放弃所有权,复制某些东西的能力是另一种特权 . 这就是Rust模仿它的方式 .

    这对我来说是一个棘手的问题!使用Rust一段时间后,移动语义是很自然的 . 让我知道我遗漏或解释不好的部分 .

  • 9

    请让我回答我自己的问题 . 我遇到了麻烦,但在这里问了一个问题我做了Rubber Duck Problem Solving . 现在我明白了:

    move 是该值的 transfer of ownership .

    例如,赋值 let x = a; 转移所有权:首先 a 拥有该值 . 在 let 之后,拥有该值的是 x . Rust禁止此后使用 a .

    事实上,如果你在 let 之后做 println!("a: {:?}", a); ,Rust编译器说:

    error: use of moved value: `a`
    println!("a: {:?}", a);
                        ^
    

    完整的例子:

    #[derive(Debug)]
    struct Example { member: i32 }
    
    fn main() {
        let a = Example { member: 42 }; // A struct is moved
        let x = a;
        println!("a: {:?}", a);
        println!("x: {:?}", x);
    }
    

    move 是什么意思?

    似乎这个概念来自C 11. A document about C++ move semantics说:

    从客户端代码的角度来看,选择移动而不是复制意味着您不关心源的状态会发生什么 .

    啊哈 . C 11并不关心源码发生了什么 . 所以在这种情况下,Rust可以自由决定禁止在移动后使用源 .

    它是如何实现的?

    我不知道 . 但是我可以想象Rust确实没什么 . x 只是相同值的不同名称 . 名称通常被编译掉(当然除了调试符号) . 因此,绑定的名称是 a 还是 x 是相同的机器代码 .

    似乎C在复制构造函数elision中也是如此 .

    什么都不做是最有效的 .

  • 1

    将 Value 转化为功能,也会导致所有权转移;它与其他例子非常相似:

    struct Example { member: i32 }
    
    fn take(ex: Example) {
        // 2) Now ex is pointing to the data a was pointing to in main
        println!("a.member: {}", ex.member) 
        // 3) When ex goes of of scope so as the access to the data it 
        // was pointing to. So Rust frees that memory.
    }
    
    fn main() {
        let a = Example { member: 42 }; 
        take(a); // 1) The ownership is transfered to the function take
                 // 4) We can no longer use a to access the data it pointed to
    
        println!("a.member: {}", a.member);
    }
    

    因此预期的错误:

    post_test_7.rs:12:30: 12:38 error: use of moved value: `a.member`
    

相关问题