Rust中Move語義下的Copy與Clone


問題

在寫Rust代碼的時候,在遇到函數、閉包甚至是循環等作用域的切換時,不知道當前要操作的對象是被borrow或者move,所以經常會報一些錯誤,想借用一些示例來測試切換作用域時Rust會做一些什么操作,也由此延伸出了Copy與Clone的操作差異

測試場景

使用多線程、閉包來模擬作用域的切換

測試對象沒有去指定Send+Sync,因為沒有涉及數據競爭

let some_obj=xxx
let handle=std::thread::spawn(move ||{
    println!("{:#?}", some_obj);
});

handle.join().unwrap();
println!("{:#?}", some_obj);

測試對象

按照Rust中的定義,可以分為2種

1 可知固定長度的對象,在其他語言中有時會使用值對象來作為定義

2 運行時動態長度的對象,一般內存在heap中,stack上分配的是指針,指向heap中的地址,其他語言中有時會使用引用對象作為定義

值對象

可以使用數字類型來代表此類對象

let num_f=21.3;
let num_i=33;
let char='a';

let handle=std::thread::spawn(move ||{
  println!("{:?} : {:#?}",std::thread::current().id(), num_f);
  println!("{:?} : {:#?}",std::thread::current().id(), num_i);
  println!("{:?} : {:#?}",std::thread::current().id(), char);
});

handle.join().unwrap();

println!("{:?} : {:#?}",std::thread::current().id(), num_f);
println!("{:?} : {:#?}",std::thread::current().id(), num_i);
println!("{:?} : {:#?}",std::thread::current().id(), char);
ThreadId(3) : 21.3
ThreadId(3) : 33
ThreadId(3) : 'a'
ThreadId(2) : 21.3
ThreadId(2) : 33
ThreadId(2) : 'a'

如果去掉move關鍵字,會有什么情況?以下是運行的結果,直接報錯

46 |     let handle=std::thread::spawn( ||{
   |                                    ^^ may outlive borrowed value `num_f`
47 |         println!("{:?} : {:#?}",std::thread::current().id(), num_f);
   |                                                              ----- `num_f` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src/thread_shared_obj/thread_test.rs:46:16
   |
46 |       let handle=std::thread::spawn( ||{
   |  ________________^
47 | |         println!("{:?} : {:#?}",std::thread::current().id(), num_f);
48 | |         println!("{:?} : {:#?}",std::thread::current().id(), num_i);
49 | |         println!("{:?} : {:#?}",std::thread::current().id(), char);
50 | |     });
   | |______^
help: to force the closure to take ownership of `num_f` (and any other referenced variables), use the `move` keyword
   |
46 |     let handle=std::thread::spawn( move ||{

may outlive borrowed value ,由此可知閉包默認使用的是borrow ,而不是move,對應的Trait是 Fn,如果是使用move關鍵字,對應的Trait就會是FnOnce

繼續看這句報錯,回過頭看代碼,可知的是,在線程中的作用域使用num_f這個變量時,由於num_f也在外面的作用域,Rust編譯器不能確定在運行時外面是否會修改這個變量,對於此種場景,Rust是拒絕編譯通過的

這里雖然有了move關鍵字,但對於值對象來說,就是copy了一個全新的值

線程中的值對象和外面作用域的值對象,此時實際上變成了2分,Copy動作是Rust編譯器自動執行的

引用對象

字符串

字符串在運行時是可以動態改變大小的,所以在stack上會有指向heap中內存的指針

let string_obj="test".to_string();

let handle=std::thread::spawn( move ||{
	println!("{:?} : {:#?}",std::thread::current().id(), string_obj);
});

handle.join().unwrap();

println!("{:?} : {:#?}",std::thread::current().id(), string_obj);

運行結果

61 |    let string_obj="test".to_string();
   |        ---------- move occurs because `string_obj` has type `String`, which does not implement the `Copy` trait
62 | 
63 |     let handle=std::thread::spawn( move ||{
   |                                    ------- value moved into closure here
64 |         println!("{:?} : {:#?}",std::thread::current().id(), string_obj);
   |                                                              ---------- variable moved due to use in closure
...
69 |     println!("{:?} : {:#?}",std::thread::current().id(), string_obj);
   |                                                          ^^^^^^^^^^ value borrowed here after move

這里會產生問題,和值對象一樣,使用了move關鍵字,但為什么字符串這里報錯了

看報錯的語句

move occurs because `string_obj` has type `String`, which does not implement the `Copy` trait

在值對象的示例中,並沒有這樣的錯誤,也由此可推斷值對象是實現了Copy Trait的,並且在作用域切換的場景中,直接使用Copy,在官方文檔中,關於Copy特別說明了是簡單的二進制拷貝。

這里可以有的猜測是,關於字符串,由於不知道運行時會是什么情況,所以無法簡單定義Copy的行為,也就是簡單的二進制拷貝,需要使用Clone來顯式指定有什么樣的操作。

官方文檔果然是這樣說的

如果這里修改一下代碼,是可以通過的

let string_obj = "test".to_string();
let string_obj_clone = string_obj.clone();

let handle = std::thread::spawn(move || {
	println!("{:?} : {:#?}", std::thread::current().id(), string_obj_clone);
});

handle.join().unwrap();

println!("{:?} : {:#?}", std::thread::current().id(), string_obj);

運行結果

ThreadId(3) : "test"
ThreadId(2) : "test"

就像值對象的處理方式,只不過這里是顯式指定clone,讓對象變成2分,各自在不同的作用域

Vec 也是同理

自定義結構體

Rust中沒有類的概念,struct實際上會比類更抽象一些

Rust設計有意思的地方也來了,可以為結構體快捷的泛化Copy,但是很不幸的是,如果是類似於String這種沒有Copy的,仍然要顯式實現Clone以及顯示調用Clone

可以Copy的結構體

結構體定義如下

#[derive(Debug,Copy,Clone)]
pub struct CopyableObj{
    num1:i64,
    num2:u64
}

impl CopyableObj{
    pub fn new(num1:i64,num2:u64) -> CopyableObj{
        CopyableObj{num1,num2}
    }
}

測試代碼如下

let st=CopyableObj::new(1,2);
let handle = std::thread::spawn(move || {
	println!("{:?} : {:#?}", std::thread::current().id(), st);
});

handle.join().unwrap();

println!("{:?} : {:#?}", std::thread::current().id(), st);

結果

ThreadId(3) : CopyableObj {
    num1: 1,
    num2: 2,
}
ThreadId(2) : CopyableObj {
    num1: 1,
    num2: 2,
}

在結構體上使用宏標記 Copy&Clone,Rust編譯器就會自動實現在move時的copy動作

不可以Copy的結構體

如果把結構體中的字段換成String

#[derive(Debug,Copy, Clone)]
pub struct UncopiableObj{
    str1:String
}

impl UncopiableObj{
    pub fn new(str1:String) -> UncopiableObj{
        UncopiableObj{str1}
    }
}

pub fn test_uncopiable_struct(){
    let st=UncopiableObj::new("test".to_string());

    let handle = std::thread::spawn(move || {
        println!("{:?} : {:#?}", std::thread::current().id(), st);
    });

    handle.join().unwrap();

    println!("{:?} : {:#?}", std::thread::current().id(), st);
}

運行

78 | #[derive(Debug,Copy, Clone)]
   |                ^^^^
79 | pub struct UncopiableObj{
80 |     str1:String
   |     ----------- this field does not implement `Copy`

如果去掉宏標記的Copy

#[derive(Debug)]
pub struct UncopiableObj{
    str1:String
}

impl UncopiableObj{
    pub fn new(str1:String) -> UncopiableObj{
        UncopiableObj{str1}
    }
}

運行

80 |     let st=UncopiableObj::new("test".to_string());
   |         -- move occurs because `st` has type `shared_obj::UncopiableObj`, which does not implement the `Copy` trait
81 | 
82 |     let handle = std::thread::spawn(move || {
   |                                     ------- value moved into closure here
83 |         println!("{:?} : {:#?}", std::thread::current().id(), st);
   |                                                               -- variable moved due to use in closure
...
88 |     println!("{:?} : {:#?}", std::thread::current().id(), st);
   |                                                           ^^ value borrowed here after move

由此可知,這里是真的move進入線程的作用域了,外面的作用域無法再使用它

仍然是使用Clone來解決這個問題,但實際上這里可以有2種Clone,1種是默認的直接全部深度Clone,另外1種則是自定義的

先看看Rust自動的Clone

#[derive(Debug,Clone)]
pub struct UncopiableObj{
    str1:String
}

impl UncopiableObj{
    pub fn new(str1:String) -> UncopiableObj{
        UncopiableObj{str1}
    }
}

pub fn test_uncopiable_struct(){
    let st=UncopiableObj::new("test".to_string());
    let st_clone=st.clone();

    let handle = std::thread::spawn(move || {
        println!("{:?} : {:#?}", std::thread::current().id(), st_clone);
    });

    handle.join().unwrap();

    println!("{:?} : {:#?}", std::thread::current().id(), st);
}

運行結果

ThreadId(3) : UncopiableObj {
    str1: "test",
}
ThreadId(2) : UncopiableObj {
    str1: "test",
}

再看看自定義的Clone

#[derive(Debug)]
pub struct UncopiableObj{
    str1:String
}
impl Clone for UncopiableObj{
    fn clone(&self) -> Self {
        UncopiableObj{str1: "hahah".to_string() }
    }
}

pub fn test_uncopiable_struct(){
    let st=UncopiableObj::new("test".to_string());
    let st_clone=st.clone();

    let handle = std::thread::spawn(move || {
        println!("{:?} : {:#?}", std::thread::current().id(), st_clone);
    });

    handle.join().unwrap();

    println!("{:?} : {:#?}", std::thread::current().id(), st);
}

運行結果

ThreadId(3) : UncopiableObj {
    str1: "hahah",
}
ThreadId(2) : UncopiableObj {
    str1: "test",
}

嵌套的結構體

如果字段實現了Copy或者Clone,則結構體可以直接使用宏標記指明,是直接泛化的。

結論

在作用域有變更的場景下,如果實現了Copy的(一般情況是知道內存占用長度的對象),在move語義中,實際上會被Rust編譯器翻譯成Copy;而沒有實現Copy的(一般情況是值不知道運行時內存占用長度的對象),在move語義中,所有權會被直接轉移到新的作用域中,原有作用域是無法再次使用該對象的。

所以沒有實現Copy的對象,在move語義中,還可以選擇顯式指定Clone或者自定義Clone。

String的Clone是已經默認實現了的,所以可以直接使用Clone的方法。

擴展結論

move語義定義了所有權的動作,值對象會自動使用Copy,但仍然可以使用borrow,例如在只讀的場景中。

由於Rust是針對內存安全的設計,所以在不同的場景下需要選擇不同的語義。

例如,沒有實現Copy的自定義結構體,

在move語義中,如果實現了Clone,其實是類似於函數式編程的無副作用;如果沒有實現Clone,則是直接轉移了所有權,只在當前作用域生效;如果想使用類似於c++的指針,則可以使用borrow(不可變或者可變,需要考慮生命周期);還有一種簡便的方法,使用Rust提供的智能指針。

這幾種在不同作用域中切換指針的方式實際上對應了不同場景的不同指針使用策略,同時也是吸收了函數式、c++智能指針、Java處理指針的方式,就是大雜燴。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM