簡體   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