問題
在寫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處理指標的方式,就是大雜燴,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/262815.html
標籤:其他
下一篇:C++指標筆記
