https://www.tuicool.com/articles/UfIvemA
2015 年 6 月 4 日
概述
“error: use of moved value”,相信最近開始玩rust的同學看到這個報錯都能會心一笑了。
rust做到了不依賴運行期垃圾回收的安全內存管理,但這個特別爽快的特性也引入了一些不便,這報錯就是常見的麻煩之一。
這報錯要是想展開說清楚,需要完整解釋rust的ownership、borrowing、lifetime等概念,那是一篇太長的文章。
我們先暫時放下概念,用三個不同的方法動手解決這個報錯。
錯誤
我們以下面這段程序為基礎展開我們的討論,這里面主要定義的就是一個Info結構體。
struct Info { s: String, } fn fn_a(info: Info) { println!("in fn_a"); } fn main() { let foo = Info {s : "abc".to_string() }; fn_a(foo); }
首先,我們要制造出報錯“use of moved value”。很簡單,我們只需要以foo為參數再調用一次fn_a()就好。
struct Info { s: String, } fn fn_a(info: Info) { println!("in fn_a"); } fn main() { let foo = Info {s : "abc".to_string() }; fn_a(foo); fn_a(foo); // 只有這行是新加入的 }
現在,我們得到了編譯器報錯。
src/main.rs:12:10: 12:13 error: use of moved value: `foo` src/main.rs:12 fn_a(foo); ^~~ src/main.rs:11:10: 11:13 note: `foo` moved here because it has type `Info`, which is non-copyable src/main.rs:11 fn_a(foo); ^~~
編譯器說,我們新加入的行里用了“移動了的值”。
啥叫“移動了的值”呢?
說白了就是用過了的值,foo已經給第一個fn_a()用過了,到了第二個fn_a()的時候就是moved value了。然后就不讓用了。
至於為什么要制定這樣的規則,我另撰文解釋。
現在我們開始動手來解決這個問題。
方法一:引用
因為我們傳給函數的是value,所以value被move了。
我們可以通過傳引用給函數來解決這問題。
代碼如下:
struct Info { pub s: String, } fn fn_a(info: &mut Info) { info.s = "bbb".to_string(); } fn main() { let mut foo = Info {s : "abc".to_string() }; println!("1: {}", foo.s); fn_a(&mut foo); println!("2: {}", foo.s); fn_a(&mut foo); println!("3: {}", foo.s); }
運行結果如下:
1: abc
2: bbb 3: bbb
我們把這段程序用impl改寫一下。下面這段程序和上面的程序其實是等價的:
struct Info { pub s: String, } impl Info { fn fn_a(&mut self) { self.s = "bbb".to_string(); } } fn main() { let mut foo = Info {s : "abc".to_string() }; println!("1: {}", foo.s); foo.fn_a(); println!("2: {}", foo.s); foo.fn_a(); println!("3: {}", foo.s); }
運行結果顯然是一樣的:
1: abc
2: bbb 3: bbb
改寫成這樣之后是不是很眼熟了呢。
沒錯,大多數的標准庫就是用的這種方法來避免moved value問題的。
impl里面的第一個參數其實就是struct的引用,所以我們用struct.fn()這種寫法的時候,傳遞給方法實際都是引用。
方法二:引用計數
rust在標准庫里提供了引用計數,它是另一個可以解決move value的方法。先列出來代碼吧。
use std::rc::Rc;
use std::cell::RefCell; struct Info { s: String, } impl Info { fn new(a: &str) -> Info { Info { s: a.to_string(), } } } fn abc(a: Rc<RefCell<Info>>) { a.borrow_mut().s = "bbbbb".to_string(); } fn main() { let bar = Rc::new(RefCell::new(Info::new("abc"))); println!("1 : {}", bar.borrow().s); abc(bar.clone()); println!("2 : {}", bar.borrow().s); abc(bar.clone()); println!("3 : {}", bar.borrow().s); }
這段代碼有點稍稍復雜。
其實需要注意的地方只有兩個:
1、使用了Rc之后,傳遞變量都要使用.clone()方法來增加引用計數;減少引用計數不用管,rust會根據作用域自己搞定;
2、變量不用聲明為mut了。使用的時候,如果不更改,使用.borrow()方法得到真正的struct;如果需要更改,則使用.borrow_mut()方法得到真正的struct。
另外還有兩點需要再說明一下:
1、如果變量根本不需要改變,則不用套里面的RefCell::new()那層;
2、如果涉及多線程之間的傳參,要放棄Rc,使用Arc。
方法三:實現Trait Clone
從方法二中可以看到,我們使用了Rc,Rc就幫助我們實現了一個.clone()方法,讓我們得以避免錯誤。
那我們能不能自己實現.clone()呢,答案當然是肯定的。
Rc做的是把數據和計數都放到了堆上,它提供的.clone()實際是對計數的復制。
我們不搞那么復雜,我們做個簡單點的,我們直接做對struct的復制。
代碼如下:
struct Info { s: i32, } impl Info { fn new(a: i32) -> Info { Info { s: a, } } } impl Clone for Info { fn clone(&self) -> Info { Info {s: self.s} } } fn abc(a: Info) -> Info { Info {s: a.s + 1} } fn main() { let mut foo = Info::new(111); println!("1 : {}", foo.s); abc(foo); println!("2 : {}", foo.s); abc(foo); println!("3 : {}", foo.s); }
運行結果:
1 : 111 2 : 111 3 : 111
為什么我們這里沒有寫foo.clone()而直接寫foo編譯器也沒抱怨什么呢?
我們可以看看最初編譯器說的話:note: foo
moved here because it has type Info
, which is non-copyable
它說,foo因為不能復制,所以它被move了。
所以當我們讓Info變得可復制了之后,它就不會被move了。
當然要寫成foo.clone(),那也是沒問題的。
但是注意因為我們的值是復制進去的,所以最原始的foo不會被改變了。
最后
以上我們用了三種方法解決了move value的報錯。
實際還有很多種方法可以用於在不同的場景中避免這個錯誤。
有興趣的同學可以繼續玩耍,最后提供一個跟蹤變量生命周期的方法以供愉快玩耍。
實現Drop Trait在變量被釋放或重分配空間時打印日志,代碼如下:
use std::fmt;
use std::rc::Rc; use std::cell::RefCell; struct Info { s: String, } impl Info { fn new(a: &str) -> Info { println!{"new:{}", a}; Info { s: a.to_string(), } } } impl Drop for Info { fn drop(&mut self) { println!("drop:{}", self.s); } } impl fmt::Display for Info { fn fmt(&self, f:&mut fmt::Formatter) -> fmt::Result { write!(f, "Info.s = {}", self.s) } } impl fmt::Debug for Info { fn fmt(&self, f:&mut fmt::Formatter) -> fmt::Result { write!(f, "Info.s = {}", self.s) } } fn main() { let foo = Info::new("abc"); let mut bar = Rc::new(RefCell::new(Info::new("abc"))); println!("1 : {:?}", bar); println!("code exit"); }