[英]How does Rust provide move semantics?
Rust 語言網站聲稱移動語義是該語言的特征之一。 但是我看不到移動語義在 Rust 中是如何實現的。
Rust 框是唯一使用移動語義的地方。
let x = Box::new(5);
let y: Box<i32> = x; // x is 'moved'
上面Rust的代碼可以寫成C++為
auto x = std::make_unique<int>(5);
auto y = std::move(x); // Note the explicit move
據我所知(如果我錯了請糾正我),
Rust 如何提供移動語義?
我認為這是來自 C++ 的一個非常普遍的問題。 在 C++ 中,當涉及到復制和移動時,您正在明確地做所有事情。 該語言是圍繞復制和引用而設計的。 在 C++11 中,“移動”東西的能力被粘在了那個系統上。 另一方面,Rust 重新開始。
Rust 根本沒有構造函數,更不用說移動構造函數了。
您不需要移動構造函數。 Rust 移動所有“沒有復制構造函數”的東西,也就是“沒有實現Copy
trait”。
struct A;
fn test() {
let a = A;
let b = a;
let c = a; // error, a is moved
}
Rust 的默認構造函數(按照慣例)只是一個名為new
的關聯函數:
struct A(i32);
impl A {
fn new() -> A {
A(5)
}
}
更復雜的構造函數應該有更具表現力的名稱。 這是 C++ 中的命名構造函數習語
不支持右值引用。
它一直是一個請求的功能,請參閱RFC 問題 998 ,但很可能您要求的是不同的功能:將內容移動到函數:
struct A;
fn move_to(a: A) {
// a is moved into here, you own it now.
}
fn test() {
let a = A;
move_to(a);
let c = a; // error, a is moved
}
無法使用右值參數創建函數重載。
你可以用特質來做到這一點。
trait Ref {
fn test(&self);
}
trait Move {
fn test(self);
}
struct A;
impl Ref for A {
fn test(&self) {
println!("by ref");
}
}
impl Move for A {
fn test(self) {
println!("by value");
}
}
fn main() {
let a = A;
(&a).test(); // prints "by ref"
a.test(); // prints "by value"
}
Rust 的移動和復制語義與 C++ 非常不同。 我將采用與現有答案不同的方法來解釋它們。
在 C++ 中,由於自定義復制構造函數,復制是一種可以任意復雜的操作。 Rust 不想要簡單賦值或參數傳遞的自定義語義,因此采用了不同的方法。
首先,在 Rust 中傳遞的賦值或參數始終只是一個簡單的內存副本。
let foo = bar; // copies the bytes of bar to the location of foo (might be elided)
function(foo); // copies the bytes of foo to the parameter location (might be elided)
但是如果對象控制了一些資源呢? 假設我們正在處理一個簡單的智能指針Box
。
let b1 = Box::new(42);
let b2 = b1;
此時,如果只復制字節,是否不會為每個對象調用析構函數(Rust 中的drop
),從而兩次釋放相同的指針並導致未定義的行為?
答案是 Rust 默認會移動。 這意味着它將字節復制到新位置,然后舊對象就消失了。 在上面第二行之后訪問b1
是編譯錯誤。 並且不會調用析構函數。 該值已移至b2
,而b1
也可能不再存在。
這就是移動語義在 Rust 中的工作方式。 字節被復制,舊對象消失了。
在一些關於 C++ 移動語義的討論中,Rust 的方式被稱為“破壞性移動”。 有人提議添加“移動析構函數”或類似於 C++ 的東西,以便它可以具有相同的語義。 但是在 C++ 中實現的移動語義不會這樣做。 舊對象被留下,它的析構函數仍然被調用。 因此,您需要一個移動構造函數來處理移動操作所需的自定義邏輯。 移動只是一個專門的構造函數/賦值運算符,它應該以某種方式運行。
所以默認情況下,Rust 的賦值移動對象,使舊位置無效。 但是許多類型(整數、浮點數、共享引用)都有語義,其中復制字節是創建真實副本的一種完全有效的方式,無需忽略舊對象。 這些類型應該實現Copy
trait,它可以由編譯器自動派生。
#[derive(Copy)]
struct JustTwoInts {
one: i32,
two: i32,
}
這向編譯器發出信號,賦值和參數傳遞不會使舊對象無效:
let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);
請注意,瑣碎的復制和銷毀的需要是相互排斥的; 類型是Copy
也不能是Drop
。
現在,當您想要復制僅復制字節還不夠的東西時,例如向量,該怎么辦? 對此沒有語言功能; 從技術上講,該類型只需要一個函數來返回一個以正確方式創建的新對象。 但按照慣例,這是通過實現Clone
trait 及其clone
功能來實現的。 事實上,編譯器也支持自動派生Clone
,它只是簡單地克隆每個字段。
#[Derive(Clone)]
struct JustTwoVecs {
one: Vec<i32>,
two: Vec<i32>,
}
let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();
並且無論何時您派生Copy
,您還應該派生Clone
,因為像Vec
這樣的容器在自己克隆時會在內部使用它。
#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }
現在,這有什么缺點嗎? 是的,實際上有一個相當大的缺點:因為將一個對象移動到另一個內存位置只是通過復制字節來完成的,沒有自定義邏輯,一個類型不能引用到它自己。 事實上,Rust 的生命周期系統使得安全地構造這樣的類型是不可能的。
但在我看來,這種權衡是值得的。
Rust 支持具有以下功能的移動語義:
所有類型都是可移動的。
默認情況下,在整個語言中向某處發送一個值是一種移動。 對於非Copy
類型,如Vec
,以下都是 Rust 中的動作:按值傳遞參數、返回值、賦值、按值模式匹配。
您在 Rust 中沒有std::move
,因為它是默認設置。 你真的一直在使用動作。
Rust 知道不能使用移動的值。 如果您有一個值x: String
並執行channel.send(x)
,將值發送到另一個線程,編譯器就會知道x
已被移動。 在移動后嘗試使用它是一個編譯時錯誤,“使用移動的值”。 如果有人引用某個值(懸空指針),則您無法移動該值。
Rust 知道不要在移動的值上調用析構函數。 移動值會轉移所有權,包括清理責任。 類型不必能夠表示特殊的“值已移動”狀態。
移動成本低,性能可預測。 它基本上是 memcpy。 返回一個巨大的Vec
總是很快——你只是復制三個詞。
Rust 標准庫在任何地方都使用並支持移動。 我已經提到了通道,它使用移動語義來安全地跨線程傳輸值的所有權。 其他優點:所有類型都支持 Rust 中的無副本std::mem::swap()
; Into
和From
標准轉換特性是按值的; Vec
和其他集合具有.drain()
和.into_iter()
方法,因此您可以粉碎一個數據結構,將所有值移出其中,並使用這些值構建一個新的數據結構。
Rust 沒有移動引用,但移動是 Rust 中一個強大且核心的概念,它提供了許多與 C++ 中相同的性能優勢,以及其他一些優勢。
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
這就是它在 memory 中的表示方式
然后讓我們將 s 分配給 t
let t = s;
這是發生了什么:
let t = s
將向量的三個 header 字段從 s 移動到 t; 現在 t 是向量的所有者。 矢量的元素保持原樣,字符串也沒有任何變化。 每個值仍然只有一個所有者。
現在 s 被釋放了,如果我寫這個
let u = s
我收到錯誤:“使用移動值: s
”
Rust 將移動語義應用於幾乎所有值的使用(復制類型除外)。 將 arguments 傳遞給函數會將所有權轉移到函數的參數; 從 function 返回一個值將所有權轉移給調用者。 構建元組將值移動到元組中。 等等。
參考例如:Jim Blandy、Jason Orendorff、Leonora FS Tindall 的編程 Rust
原始類型不能為空並且大小固定,而非原始類型可以增長並且可以為空。 由於基本類型不能為空且大小固定,因此分配 memory 來存儲它們並處理它們相對容易。 然而,非基元的處理涉及計算隨着它們的增長將占用多少 memory 以及其他昂貴的操作。Wwith primitives rust 將制作副本,非原始 rust 執行移動
fn main(){
// this variable is stored in stack. primitive types are fixed size, we can store them on stack
let x:i32=10;
// s1 is stored in heap. os will assign memory for this. pointer of this memory will be stored inside stack.
// s1 is the owner of memory space in heap which stores "my name"
// if we dont clear this memory, os will have no access to this memory. rust uses ownership to free the memory
let s1=String::from("my name");
// s1 will be cleared from the stack, s2 will be added to the stack poniting the same heap memory location
// making new copy of this string will create extra overhead, so we MOVED the ownership of s1 into s2
let s2=s1;
// s3 is the pointer to s2 which points to heap memory. we Borrowed the ownership
// Borrowing is similar borrowing in real life, you borrow a car from your friend, but its ownership does not change
let s3=&s2;
// this is creating new "my name" in heap and s4 stored as the pointer of this memory location on the heap
let s4=s2.clone()
}
當我們將原始或非原始類型 arguments 傳遞給 function 時,同樣的原則適用:
fn main(){
// since this is primitive stack_function will make copy of it so this will remain unchanged
let stack_num=50;
let mut heap_vec=vec![2,3,4];
// when we pass a stack variable to a function, function will make a copy of that and will use the copy. "move" does not occur here
stack_var_fn(stack_num);
println!("The stack_num inside the main fn did not change:{}",stack_num);
// the owner of heap_vec moved here and when function gets executed, it goes out of scope so the variable will be dropped
// we can pass a reference to reach the value in heap. so we use the pointer of heap_vec
// we use "&"" operator to indicate that we are passing a reference
heap_var_fn(&heap_vec);
println!("the heap_vec inside main is:{:?}",heap_vec);
}
// this fn that we pass an argument stored in stack
fn stack_var_fn(mut var:i32){
// we are changing the arguments value
var=56;
println!("Var inside stack_var_fn is :{}",var);
}
// this fn that we pass an arg that stored in heap
fn heap_var_fn(var:&Vec<i32>){
println!("Var:{:?}",var);
}
我想補充一點,沒有必要轉移到memcpy
。 如果堆棧上的對象足夠大,Rust 的編譯器可能會選擇傳遞對象的指針。
在 C++ 中,類和結構的默認分配是淺拷貝。 值被復制,但不是指針引用的數據。 所以修改一個實例會改變所有副本的引用數據。 值(fe 用於管理)在另一個實例中保持不變,可能導致不一致的狀態。 移動語義避免了這種情況。 具有移動語義的內存管理容器的 C++ 實現示例:
template <typename T>
class object
{
T *p;
public:
object()
{
p=new T;
}
~object()
{
if (p != (T *)0) delete p;
}
template <typename V> //type V is used to allow for conversions between reference and value
object(object<V> &v) //copy constructor with move semantic
{
p = v.p; //move ownership
v.p = (T *)0; //make sure it does not get deleted
}
object &operator=(object<T> &v) //move assignment
{
delete p;
p = v.p;
v.p = (T *)0;
return *this;
}
T &operator*() { return *p; } //reference to object *d
T *operator->() { return p; } //pointer to object data d->
};
這樣的對象會被自動垃圾回收,並且可以從函數返回給調用程序。 它非常高效,並且與 Rust 的功能相同:
object<somestruct> somefn() //function returning an object
{
object<somestruct> a;
auto b=a; //move semantic; b becomes invalid
return b; //this moves the object to the caller
}
auto c=somefn();
//now c owns the data; memory is freed after leaving the scope
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.