1. 程式人生 > 實用技巧 >Moves, copies and clones in Rust

Moves, copies and clones in Rust

https://zhuanlan.zhihu.com/p/184907190

Introduction

Moves and copies are fundamental concepts in Rust. These might be completely new to programmers coming from garbage collected languages like Ruby, Python or C#. While these terms do exist in C++, their meaning in Rust is subtly different. In this post I'll explain what it means for values to be moved, copied or cloned in Rust. Let's dive in.

Moves

As shown inMemory safety in Rust - part 2, assigning one variable to another transfers the ownership to the assignee:

let v:Vec<i32> = Vec::new();
let v1 = v;//v1 is the new owner

In the above example,vis moved tov1. But what does it mean tomovev? To understand that, we need to see how a

Vecis laid out in memory:

AVechas to maintain a dynamically growing or shrinking buffer. This buffer is allocated on the heap and contains the actual elements of theVec. In addition, aVecalso has a small object on the stack. This object contains some housekeeping information: a pointer to the buffer on the heap, the capacity of the buffer and the length (i.e. how much of the capacity is currently filled).

When the variablevis moved tov1, the object on the stack is bitwise copied:

NOTENOTEWhat has essentially happened in the previous example is a shallow copy. This is in stark contrast to C++, which makes a deep copy when a vector is assigned to another variable.

The buffer on the heap stays intact. This is indeed a move: it is nowv1's responsibility to drop the heap buffer andvcan't touch it:

let v: Vec<i32> = Vec::new();
let v1 = v;
println!("v's length is {}", v.len());//error: borrow of moved value: `v`

This change of ownership is good because if access was allowed through bothvandv1then you will end up with two stack objects pointing to the same heap buffer:

Which object should drop the buffer in this case? Because that is not clear, Rust prevents this situation from arising at all.

Assignment is not the only operation which involves moves. Values are also moved when passed as arguments or returned from functions:

let v: Vec<i32> = Vec::new();
//v is first moved into print_len's v1
//and then moved into v2 when print_len returns it
let v2 = print_len(v);
fn print_len(v1: Vec<i32>) -> Vec<i32> {
    println!("v1's length is {}", v1.len());
    v1//v1 is moved out of the function
}

Or assigned to members of a struct or enum:

struct Numbers {
    nums: Vec<i32>
}
let v: Vec<i32> = Vec::new();
//v moved into nums field of the Numbers struct
let n = Numbers { nums: v };

enum NothingOrString {
    Nothing,
    Str(String)
}
let s: String = "I am moving soon".to_string();
//s moved into the enum
let nos = NothingOrString::Str(s);

That's all about moves. Next let's take a look at copies.

Copies

Remember this example from above?:

let v: Vec<i32> = Vec::new();
let v1 = v;
println!("v's length is {}", v.len());//error: borrow of moved value: `v`

What happens if we change the type of the variablesvandv1fromVectoi32:

let v: i32 = 42;
let v1 = v;
println!("v is {}", v);//compiles fine, no error!

This is almost the same code. Why doesn't the assignment operator movevintov1this time? To see that, let's take a look at the memory layout again:

In this example the values are contained entirely in the stack. There is nothing to own on the heap. That is why it is ok to allow access through bothvandv1— they are completely independent copies.

Such types which do not own other resources and can be bitwise copied are calledCopytypes. They implement theCopymarker trait. All primitive types like integers, floats and characters areCopy. Structs or enums are notCopyby default but you can derive theCopytrait:

#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Copy, Clone)]
enum SignedOrUnsignedInt {
    Signed(i32),
    Unsigned(u32),
}
NOTENOTEClonein the derive clause is needed becauseCopyis defined like this:pub trait Copy: Clone {}

For#[derive(Copy, Clone)]to work, all the members of the struct or enum must beCopythemselves. For example, this will not work:

//error:the trait `Copy` may not be implemented for this type
//because its nums field does not implement `Copy`
#[derive(Copy, Clone)]
struct Numbers {
    nums: Vec<i32>
}

You can of course also implementCopyandClonemanually:

struct Point {
    x: i32,
    y: i32,
}

//no method in Copy because it is a marker trait
impl Copy for Point {}

impl Clone for Point {
    fn clone(&self) -> Point {
        *self
    }
}

In general, any type that implementsDropcannot beCopybecauseDropis implemented by types which own some resource and hence cannot be simply bitwise copied. ButCopytypes should be trivially copyable. Hence,DropandCopydon't mix well.

And that's all about copies. On to clones.

Clones

When a value is moved, Rust does a shallow copy; but what if you want to create a deep copy like in C++? To allow that, a type must first implement theClonetrait. Then to make a deep copy, client code should call theclonemethod:

let v: Vec<i32> = Vec::new();
let v1 = v.clone();//ok since Vec implements Clone
println!("v's length is {}", v.len());//ok

This results in the following memory layout after theclonecall:

Due to deep copying, bothvandv1are free to independently drop their heap buffers.

NOTENOTETheclonemethod doesn't always create a deep copy. Types are free to implementcloneany way they want, but semantically it should be close enough to the meaning of duplicating an object. For example,RcandArcincrement a reference count instead.

And that's all about clones.

Conclusion

In this post I took a deeper look at semantics of moves, copies and clones in Rust. I have tried to capture the nuance in meaning when compared with C++.

Rust is great because it has great defaults. For example, the assignment operator in Rust either moves values or does trivial bitwise copies. In C++, on the other hand, an innocuous looking assignment can hide loads of code that runs as part of overloaded assignment operators. In Rust, such code is brought into the open because the programmer has to explicitly call theclonemethod.

One could argue that both languages make different trade-offs but I like the extra safety guarantees Rust brings to the table due to these design choices.

  1. copy 語義的發生場景

    copy 會發生在以下場景中

  • 變數賦值

    let a = b; // copy
  • 函式傳參

    fn say(word: i32) {}
    
    let c = 32;
    
    say(c); // copy
  • 函式返回值

    fn give_you() -> i32 {
    	return 23;
    }
  • 模式匹配 match

    struct A {
    	i: i32
    };
    
    let a = A { i: 32 };
    
    match a {
    	A {i} => println!("###, {}", i); // copy
    };
  • 模式匹配 let

    struct A {
    	i: i32
    };
    
    let a = A { i: 32 };
    
    let A { i: x }  = a; // copy
  • 閉包 capture

    let a = 32;
    let c = vec![1,2,3,4];
    let d: Vec<i32> = c
    	.iter()
    	.map(|v| v + a) // copy a
    	.collect();


    Example
    Some Rust types implement the Copy trait. Types that are Copy can be moved without owning the value in question. This is because the contents of the value can simply be copied byte-for-byte in memory to produce a new, identical value. Most primitives in Rust (bool, usize, f64, etc.) are Copy.
    
    let x: isize = 42;
    let xr = &x;
    let y = *xr; // OK, because isize is Copy
    // both x and y are owned here
    Notably, Vec and String are not Copy:
    
    let x = Vec::new();
    let xr = &x;
    let y = *xr; // ERROR, cannot move out of borrowed content