Rust 中的移动语义是什么?

What are move semantics in Rust?

在 Rust 中,引用有两种可能

  1. 借用,即引用但不允许改变引用目标。 & 运算符从值中借用所有权。

  2. 可变借用,即引用改变目标。 &mut 运算符可变地从值借用所有权。

Rust documentation about borrowing rules 说:

First, any borrow must last for a scope no greater than that of the owner. Second, you may have one or the other of these two kinds of borrows, but not both at the same time:

  • one or more references (&T) to a resource,
  • exactly one mutable reference (&mut T).

我认为获取引用就是创建一个指向该值的指针并通过该指针访问该值。如果有更简单的等效实现,编译器可以优化这一点。

但是,我不明白move是什么意思以及它是如何实现的。

对于实现 Copy 特性的类型,它意味着复制,例如通过从源中按成员方式分配结构,或 memcpy()。对于小型结构或基元,此副本很有效。

然后移动?

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

当您移动 一个项目时,您就是在转让该项目的所有权。这是 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);
}

how it is implemented.

从概念上讲,移动某物不需要做任何事情。在上面的示例中,没有理由在某处实际分配 space 然后在我分配给不同变量时移动分配的数据。我实际上不知道编译器做了什么,它可能会根据优化级别而变化。

虽然出于实际目的,您可以认为当您移动某物时,表示该项目的位会像通过 memcpy 一样被复制。这有助于解释当您将变量传递给 使用 的函数时会发生什么,或者当您 return 来自函数的值时会发生什么(同样,优化器可以做其他事情来使其高效,这只是概念上的):

// 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 时,sourcedestination 都有效复制后使用!

移动语义的一种思考方式与复制语义相同,但增加了被移动的对象不再是有效项目的限制。

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

这对我来说是个难题!使用 Rust 一段时间后,移动语义就很自然了。让我知道我遗漏或解释不当的部分。

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

移动是价值的所有权转移

例如赋值 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);
}

这个移动是什么意思?

这个概念好像来自C++11。 document about C++ move semantics 说:

From a client code point of view, choosing move instead of copy means that you don't care what happens to the state of the source.

啊哈。 C++11 不关心源代码会发生什么。因此,在这种情况下,Rust 可以自由决定在移动后禁止使用源。

它是如何实现的?

我不知道。但我可以想象 Rust 几乎什么都不做。 x 只是相同值的不同名称。名称通常被编译掉(当然调试符号除外)。因此无论绑定的名称是 a 还是 x.

都是相同的机器码

C++ 似乎在复制构造函数省略中做同样的事情。

什么都不做是最有效率的。

将值传递给函数,也会导致所有权转移;它与其他示例非常相似:

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`

语义

Rust 实现了所谓的 Affine Type System:

Affine types are a version of linear types imposing weaker constraints, corresponding to affine logic. An affine resource can only be used once, while a linear one must be used once.

不是 Copy 并因此被移动的类型是仿射类型:您可以使用它们一次或永远不会,没有别的。

Rust 在其以所有权为中心的世界观 (*) 中将此定义为 所有权转移

(*) 一些从事 Rust 工作的人比我从事 CS 工作的人更有资格,他们有意实施了仿射类型系统;然而与公开 math-y/cs-y 概念的 Haskell 相反,Rust 倾向于公开更多实用的概念。

注意:可以说仿射类型 return 从标记为 #[must_use] 的函数中编辑实际上是我阅读的线性类型。


实施

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

在函数体内 (playground):

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

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

%_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 然后到 t 的结果调用 memcpy。虽然它可能看起来效率低下,但在发布模式下检查相同的 IR,您会发现 LLVM 已完全删除副本(意识到 s 未被使用)。

调用函数时会发生同样的情况:理论上你 "move" 将对象放入函数堆栈帧中,但实际上如果对象很大,rustc 编译器可能会转而传递指针。

另一种情况是从函数 returning,但即便如此,编译器也可能应用 "return value optimization" 并直接在调用者的堆栈框架中构建——也就是说,调用者传递一个指针来写入 return 值,该值在没有中间存储的情况下使用。

Rust 的 ownership/borrowing 约束实现了 C++ 中难以实现的优化(它也有 RVO,但在许多情况下无法应用)。

因此,摘要版本:

  • 移动大对象效率低下,但有许多优化可能会完全消除移动
  • 移动涉及 memcpystd::mem::size_of::<T>() 字节,因此移动一个大的 String 是有效的,因为它只复制几个字节,无论它们持有的分配缓冲区的大小如何

Rust 的 move 关键字总是困扰着我,所以我决定写下我在与同事讨论后得到的理解。

我希望这可能对某人有所帮助。

let x = 1;

上面的语句中,x是一个值为1的变量,现在,

let y = || println!("y is a variable whose value is a closure");

因此,move关键字用于将变量的所有权转移到闭包。

在下面的示例中,没有 movex 不属于闭包。因此 x 不属于 y,可供进一步使用。

let x = 1;
let y = || println!("this is a closure that prints x = {}". x);

另一方面,在下一个例子中,x 由闭包拥有。 xy 所有,无法进一步使用。

let x = 1;
let y = move || println!("this is a closure that prints x = {}". x);

owning 我的意思是 containing as a member variable。上面的示例案例与以下两个案例的情况相同。我们还可以假设以下关于 Rust 编译器如何扩展上述情况的解释。

formar(没有move;即没有所有权转移),

struct ClosureObject {
    x: &u32
}

let x = 1;
let y = ClosureObject {
    x: &x
};

后者(与move;即所有权转让),

struct ClosureObject {
    x: u32
}

let x = 1;
let y = ClosureObject {
    x: x
};
let s1:String= String::from("hello");
let s2:String= s1;

为了确保内存安全,rust 使 s1 无效,所以这不是浅拷贝,而是调用 Move

fn main() {
  // Each value in rust has a variable that is called its owner
  // There can only be one owner at a time.
  let s=String::from('hello')
  take_ownership(s)
  println!("{}",s)
  // Error: borrow of moved value "s". value borrowed here after move. so s cannot be borrowed after a move
  // when we pass a parameter into a function it is the same as if we were to assign s to another variable. Passing 's' moves s into the 'my_string' variable then `println!("{}",my_string)` executed, "my_string" printed out. After this scope is done, some_string gets dropped. 

  let x:i32 = 2;
  makes_copy(x)
  // instead of being moved, integers are copied. we can still use "x" after the function
  //Primitives types are Copy and they are stored in stack because there size is known at compile time. 
  println("{}",x)
}

fn take_ownership(my_string:String){
  println!('{}',my_string);
}

fn makes_copy(some_integer:i32){
  println!("{}", some_integer)
}