Rust中Move語義下的Copy與Clone

問題

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

測試場景

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

測試對象沒有去指定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就會是FnOnceui

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

這裏雖然有了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處理指針的方式,就是大雜燴。

相關文章
相關標籤/搜索