Rust中的移动语义是什么?

47
在Rust中,有两种可能性可以引用一个值:
1. 借用(Borrow),即引用但不允许修改引用的目标。&运算符从一个值中借用所有权。
2. 可变借用(Borrow mutably),即引用以修改目标。&mut运算符从一个值中可变地借用所有权。
Rust文档对于借用规则的说明如下:
首先,任何借用都必须持续到其所有者的范围内。其次,你可以拥有这两种类型的借用中的一种,但不能同时拥有两种:
- 一个或多个对资源的引用(&T)。 - 恰好一个可变引用(&mut T)。 Rust借用规则的文档

我认为引用是创建一个指向值的指针,并通过指针访问该值。如果存在更简单的等效实现,编译器可以对其进行优化。

然而,我不理解移动的含义以及它是如何实现的。

对于实现Copy特质的类型,这意味着通过源结构逐个成员地赋值或使用memcpy()进行复制。对于小型结构体或基本类型,此复制是高效的。

那么移动又是什么呢?

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


2
你可能会对Rust中的Move vs CopyRust如何提供移动语义?感兴趣。 - Shepmaster
看起来你已经找到了答案,但我现在也在学习这个东西,我发现这些资源非常有帮助 https://doc.rust-lang.org/book/ownership.html 和 https://www.youtube.com/watch?v=WQbg6ZMQJvQ - Akavall
1
根据这本书,https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ways-variables-and-data-interact-move。移动是浅拷贝+失效: "如果你在使用其他语言时听说过浅拷贝和深拷贝这些术语,那么仅仅拷贝指针、长度和容量而不拷贝数据的概念可能听起来像是浅拷贝。但由于Rust还使第一个变量失效,所以它被称为移动,而不是浅拷贝"。 - mslot
6个回答

43

语义学

Rust实现了一种被称为线性类型系统的东西:

线性类型是一种弱约束的线性类型,对应于线性逻辑。 线性资源最多只能使用一次,而线性资源必须恰好使用一次

不是Copy类型的类型,因此被移动的类型是线性类型:您可以将它们使用一次或者永不使用,没有其他选择。

Rust将这称为其以所有权为中心的世界观中的所有权转移

(*) Rust的一些开发人员在计算机科学方面比我更有资格,并且他们有意实现了线性类型系统;然而,与Haskell不同,Haskell暴露了更多数学和计算机科学的概念,Rust倾向于暴露更多实用的概念。

注意:可以争论的是,从带有#[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,然后到tmemcpy。虽然看起来效率低下,但在Release模式下检查相同的IR,你会意识到LLVM已经完全省略了这些拷贝(意识到s没有被使用)。
当调用函数时,同样的情况也会发生:理论上,你将对象"移动"到函数的堆栈帧中,但实际上,如果对象很大,rustc编译器可能会切换到传递指针的方式。
另一种情况是从函数中返回,但即使如此,编译器也可能应用"返回值优化",直接在调用者的堆栈帧中构建--也就是说,调用者传递一个指针来写入返回值,而不需要中间存储。
Rust的所有权/借用约束使得一些在C++中难以实现的优化成为可能(虽然C++也有返回值优化,但不能在那么多情况下应用)。
所以,简而言之:
搬运大物体效率低下,但有一些优化方法可以避免搬运操作。 搬运涉及到对std::mem::size_of::<T>()字节的memcpy操作,所以搬运大的String对象是高效的,因为它只复制了几个字节,无论分配的缓冲区大小如何。

2
什么是仿射类型的用例?为什么防止多次使用值很有用? - weberc2
2
@weberc2:想象一下,你有一个存储在堆上的字符串。通过移动它,您可以避免引用计数或垃圾回收的需要,因为没有人可以访问“旧”绑定。好吧,事实证明,还有其他用例需要避免重用“旧”绑定。例如,在状态机中,一旦从状态转换出来,再次为该状态进行转换就没有意义了。移动语义有助于在编译时对此进行建模。 - Matthieu M.
1
关于你的字符串例子,移动只是创建了使用-after-free的问题,而不是解决它。通常,事件的顺序应该是:分配字符串,使用字符串调用func1,使用字符串调用func2,销毁字符串。只是将析构函数移到func1中,从而防止func2安全运行。这几乎从来不是我想要的,它排除了大量的易证明的有效程序。也许有一些用途,但它们似乎很少见——特别是我很确定我可以在没有关联类型的语言中制作出静态保证状态机。 - weberc2
2
@weberc2: 特别是,我相信我可以在没有关联类型的语言中制作一个静态保证状态机 => 如果你真的做到了,我很想知道如何做到(我必须在工作中使用C++,需要更多的静态保证)。至于func1func2,请注意,使用关联类型系统会导致编译时错误,因此不存在使用后释放问题;解决方案是通过引用传递,或使func1返回字符串。前者需要借用检查才能安全,后者需要一些扭曲。 - Matthieu M.
@weberc2:我只知道两种在静态的情况下防止使用后释放的有效方法:仿射类型和内存区域(关于后者,请参见Cyclone语言)。而后者无法与union/sum类型一起工作。例如,在Rust中,&mut T是仿射的,而不是可复制的,以保证内存安全,我不知道如何做到没有它。至于状态机:每个状态一个类型,每个转换一个函数(使用状态、可能的其他参数,生成新状态)。 - Matthieu M.
显示剩余6条评论

18

当您移动一个项目时,您正在转移所有权。这是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 } } 

"等等!",你说道,"memcpy 只适用于实现了 Copy 的类型!"。这大体上是正确的,但最主要的区别在于当一个类型实现了Copy时,无论是源还是目的地,在复制之后都是有效的!

移动语义的一种思考方式与复制语义相同,但增加了一个限制,即从中移动的对象不再是可用的。

然而,更容易的思考方式是:最基本的事情是移动/转让所有权,而复制某些东西是一种附加的特权。这就是Rust的模型方式。

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


4
我还想补充一点,移动一个变量不仅会防止进一步使用它,也会禁用对该变量运行析构函数。这与C ++不同,因为在C ++中,据我所知,必须显式设计类型以允许移动,因为它们的析构函数始终会被运行,因此移动构造函数必须确保析构函数不会执行任何愚蠢的操作。 - Vladimir Matveev
5
@nalply,确实,C++11确实使用了“移动语义”这个术语,并正式将其引入到语言中,但是,移动语义的概念已经存在了很长时间。然而,这是程序员必须手动跟踪的内容。Rust将这个棘手的主题提升为一流公民,并使其更难自己绊倒,这是我觉得这种语言非常有趣的部分之一! - Shepmaster
2
@Shepmaster:是的,与C++相反,Rust实现了一种仿射类型系统,值可以被使用(或“消耗”)最多一次。由于这个特性,Rust允许实现“状态机”,并且这些状态机可以通过编译器进行类型检查。我已经看到许多库利用了这个特性。 - Matthieu M.
3
@kirbyfan64sos在吹毛求疵,这个操作始终复制值(关于优化器的注意事项已经在帖子中提到)。但是,具有堆分配组件(BoxVecString等)的值是使用一个结构构建的,该结构在概念上具有指向数据的指针。指针被复制,指向的数据没有被复制(这是Clone trait所涉及的领域)。确实如你所说,大块内存并没有被移动。 - Shepmaster
当你移动一个物品时,你正在转移该物品的所有权。我一直在阅读这个,但并不明显为什么这是一个期望的属性。 - weberc2
显示剩余4条评论

2

我对Rust中的move关键字一直感到困扰,因此我决定写下我在与同事讨论后得出的理解。

我希望这可能会帮助某些人。

let x = 1;

在上述语句中,x是一个变量,其值为1。现在,
let y = || println!("y is a variable whose value is a closure");

所以,“move”关键字用于将变量的所有权转移给闭包。
在下面的例子中,如果没有“move”,则“x”不属于闭包。因此,“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);

我所说的“拥有”是指“作为成员变量包含”。上面的例子情况与以下两种情况相同。我们也可以假设下面的解释是Rust编译器如何扩展上述情况。

前者(没有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
};

1
请允许我回答自己的问题。我遇到了麻烦,但在这里提问后,我进行了橡皮鸭子问题解决法。现在我明白了: 移动价值所有权的转移
例如,赋值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。一篇C++移动语义(move semantics)的文章说:

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

啊哈。C++11不关心源的状态。所以在这方面,Rust可以自由地决定禁止在移动之后使用源。

那它是如何实现的呢?

我不知道。但我可以想象Rust实际上什么也没做。x只是同一个值的另一个名称。通常名称会被编译掉(当然除了调试符号)。因此,无论绑定具有名称a还是x,它都是相同的机器码。

似乎C++在复制构造函数省略中也是这样做的。

什么也不做是最有效的。


2
实际上,Rust 可能会做一些事情。在 Rust 中,当您按值传递时,它会将该值移动到函数帧中,在这种情况下,移动可能最终是物理的(memcpy)。 - Matthieu M.

1
将值传递给函数,也会导致所有权的转移;这与其他示例非常相似:
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`

0
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)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接