繁体   English   中英

Rust 中的移动语义是什么?

[英]What are move semantics in Rust?

在 Rust 中,有两种可能引用

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

  2. 可变地借用,即获取一个引用来改变目的地。 &mut运算符可变地从值借用所有权。

关于借用规则Rust 文档说:

首先,任何借用的持续范围不得超过所有者的范围。 其次,您可能拥有这两种借款中的一种,但不能同时拥有:

  • 对资源的一个或多个引用( &T ),
  • 正好是一个可变引用( &mut T )。

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

但是,我不明白移动意味着什么以及它是如何实施的。

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

而对于移动

这个问题不是什么是移动语义的重复 因为 Rust 和 C++ 是不同的语言,并且两者之间的移动语义不同。

语义

Rust 实现了所谓的仿射类型系统

仿射类型是线性类型的一个版本,它施加较弱的约束,对应于仿射逻辑。 仿射资源只能使用一次,而线性资源必须使用一次。

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

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

(*) 一些在 Rust 工作的人比我在 CS 中更有资格,他们故意实施了仿射类型系统; 然而,与 Haskell 公开 math-y/cs-y 概念相反,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然后到t的结果调用memcpy 虽然它可能看起来效率低下,但在 Release 模式下检查相同的 IR,您会发现 LLVM 已经完全省略了副本(意识到s未使用)。

调用函数时会发生相同的情况:理论上您将对象“移动”到函数堆栈框架中,但实际上如果对象很大,rustc 编译器可能会切换为传递指针。

另一种情况是从函数返回,但即使如此,编译器也可能应用“返回值优化”并直接在调用者的堆栈帧中构建——也就是说,调用者传递一个指针来写入返回值,该指针不使用中间存储。

Rust 的所有权/借用限制实现了在 C++ 中难以达到的优化(它也有 RVO,但不能在很多情况下应用它)。

所以,摘要版本:

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

当您移动一个项目时,您正在转移该项目的所有权 这是 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 一段时间后,移动语义很自然。 让我知道我遗漏了哪些部分或解释得不好。

请让我回答我自己的问题。 我遇到了麻烦,但通过在这里提出问题,我完成了橡皮鸭问题解决 现在我明白了:

移动是价值所有权转移

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

事实上,如果你执行println!("a: {:?}", a); let 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。 关于 C++ 移动语义的文档说:

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

啊哈。 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 的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 编译器如何扩展上述情况的解释。

形式(没有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
};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM