Ownership是Rust最獨特的一個特性,可以保證內存安全。數組
以string爲例:安全
{ // s尚未被聲明,不有效 let s = "hello"; // 從這裏開始,s有效 // 用s搞事情 } // 這個做用域結束了,s再也不有效
簡單來講:數據結構
String
類型爲了更好地闡述Ownership的規則,咱們須要一個比以前學得更復雜的數據類型。以前的數據類型都存儲在棧(stack)中,並在它們的做用域結束以後從棧中彈出(pop off)。而String
存儲在堆(heap)中,並經過指針(pointer)訪問。函數
在這裏咱們只集中討論String
的和ownership有關的部分,這些方面也適用於standard library中的其它複雜數據類型。操作系統
上面的例子中,s爲字符串常量,它不能夠改變。並且必須在代碼中將字符串的值寫出。用接下來的代碼能夠建立可變的、沒必要在代碼中將字符串的值寫出的String
:指針
let mut s = String::from("hello"); s.push_str(", world!"); println!("{}", s); // `hello, world!`
::
→ 命名空間,使from
函數具體指代String
類下的from
函數。push_str()
→ 在一個String後拼接一個字符串。爲何String
能夠被改變可是字符串常量(literals)不行?區別就在這兩種類型在內存中的存儲方式。code
對於字符串常量,在編譯時咱們便知道其內容,因此其文本將直接被寫入最終的可執行文件。所以字符串常量又快又有效率。ip
對於String
類型,爲了使其內容可變,咱們須要在堆上分配必定數量(在編譯時不知道)的內存來存儲內容。這意味着:內存
String
後歸還內存的方法。第一部分能夠由String::from
完成。ci
第二部分在Rust中是自動完成的:若是owner脫離了做用域,那麼內存也將自動地被歸還(drop
)。
整數例:
let x = 5; let y = x; //將5綁定到x上,而後複製一份x的值並使其綁定到y上。
String
例:
let s1 = String::from("hello"); let s2 = s1;
String
類由三部分組成:指針(pointer)、長度(length)、容量(capacity)。這三部分數據都存在棧中。
其中指針指向堆中字符串的首個字母的地址,長度表明String
正在使用的內存,容量表明操做系統分配給String
的總內存量(單位都是字節)。
當咱們let s2 = s1;
時,String
數據被複制了。這意味着咱們複製的是指針、長度、容量,而不是在堆中的字符串自己。
可是以前咱們說過,當一個變量脫離做用域時,Rust會自動歸還其內存。可是這裏咱們的s1
和s2
都指向同一個內存,那麼這會不會引發double free錯誤呢?
爲了保證內存安全,在進行let s2 = s1;
以後,Rust就認爲s1
再也不有效,所以當s1
脫離做用域時也沒必要歸還內存。如:
let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1);
編譯時將會拋出一個CE。
由於在複製指針、長度、容量以後,s1
再也不有效,因此咱們能夠說是s1
被移動(move)到了s2
。
若是咱們想要複製的是String
在堆中的數據,而不只僅是棧中的數據,咱們能夠用一個經常使用方法clone
。如:
let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2);
此時s2
是s1
的一份複製,包括棧中的數據與堆中的數據,此時s1
仍然有效。可是要付出運行效率的代價。
如下的一段代碼可以運行,並且有效:
let x = 5; let y = x; println!("x = {}, y = {}", x, y);
可是這看起來好像和以前所說的相悖:咱們沒有用clone
,可是x
仍然有效,沒有被移動到y
。
緣由是,像整數類型這種在編譯時就有固定的size的數據類型,徹底被存儲在棧上,所以建立副本很方便快捷。因此讓x
在被y
複製後仍然有效對運行效率的影響並不大,也沒理由不這麼作。因此調用clone
與否並無多大區別,也沒必要管它。(固然,要是願意的話,恁也能夠寫let y = x.clone()
)
在Rust中,Stack-Only Data有一個叫作Copy
的特性(trait)。若是一個類型有Copy
特性,那麼被用來賦值(assign)的變量仍然有效。若是一個變量脫離了做用域,咱們又想對它使用Copy
,那麼咱們會獲得一個CE。
幾種常見的可Copy
類型:
u32
bool
,包括true
和false
f64
char
Copy
類型的Tuple,好比(i32, i32)
可Copy
,而(i32, String)
不能夠。將一個值傳遞給一個函數的原理與將一個值賦值給一個變量是類似的,要麼move,要麼copy
。如:
fn main() { let s = String::from("hello"); // s進入做用域 takes_ownership(s); // s的值被move到函數裏,所以在這裏再也不有效 let x = 5; // x進入做用域 makes_copy(x); // x被move到函數裏,可是由於i32可Copy,所以x仍然有效 } // x脫離做用域。而後是s,可是由於s的值已經被move了,因此沒有什麼特別的發生 fn takes_ownership(some_string: String) { // some_string進入做用域 println!("{}", some_string); } // some_string脫離做用域,內存釋放 fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{}", some_integer); } // some_integer脫離做用域,沒有什麼特別的發生
若是咱們嘗試在調用takes_ownership()
後使用s
,則會拋出一個CE。
返回值也能夠轉移ownership,好比:
fn main() { let s1 = gives_ownership(); // gives_ownership()將它的返回值move到s1 let s2 = String::from("hello"); // s2進入做用域 let s3 = takes_and_gives_back(s2); // s2被move到takes_and_gives_back()裏, // takes_and_gives_back()將它的返回值move到s3 } // s3脫離做用域並釋放內存。s2脫離做用域,可是由於它被move,所以沒有什麼特別的發生。 // s1脫離做用域並釋放內存。 fn gives_ownership() -> String { // gives_ownership會將它的返回值move到 // 調用它的函數裏 let some_string = String::from("hello"); // some_string進入做用域 some_string // some_string被return,並被move到調用的函數 } fn takes_and_gives_back(a_string: String) -> String { // a_string進入做用域 a_string // a_string被return, 並被move到調用的函數 }
簡單來講,當一個值被賦值給另外一個變量時,這個值被move。當一個在堆上有數據的變量脫離做用域時,它的值會被drop
清除,除非它被move
到另外一個變量上。
若是咱們想要讓一個函數在不取走ownership的狀況下使用一個值怎麼辦?一個方法是返回傳遞的值——將它與函數的返回值一塊兒返回,如返回一個tuple:
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len()返回一個String的長度(length) (s, length) }
但這太麻煩了……
利用引用能夠方便地讓函數在不取走ownership的狀況下使用一個值。如:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { // s是一個String類的引用 s.len() } // s脫離做用域。可是因爲它沒有它指向的值的ownership,什麼都不會發生
&
→ references引用可讓恁在不取走ownership的狀況下使用一個值。
咱們稱呼這種將引用做爲函數參數的行爲爲借用(Borrowing)。
可是若是想要修改正在借用的值,則會拋出一個CE。
就像變量默認是不可變的,引用默認也不可變。
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
爲了使引用可變,首先,咱們得使s
變成mut
,而後用&mut s
建立一個可變引用,而後在函數的參數列表裏寫some_string: &mut String
來接受它。
可是可變引用有一個很大的限制:在一個特定的域中只能有對一段特定的數據的可變引用。如:
fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); }
這樣建立兩個引用則會在編譯時拋出一個CE。
Rust中有這種限制,是爲了防止data race:
Data races會引發難以檢查判斷的RE,爲了防止這個問題,Rust直接在編譯時就將它們報錯。
咱們能夠運用{}
建立多個不同時的可變引用:
let mut s = String::from("hello"); { let r1 = &mut s; } // r1脫離了做用域,因此咱們能夠建立一個新的可變引用 let r2 = &mut s;
類似地,咱們不能在有一個不可變引用的同時建立一個可變引用,可是能夠同時建立多個不可變引用:
let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem let r3 = &mut s; // BIG PROBLEM println!("{}, {}, and {}", r1, r2, r3);
此外,一個引用的做用域從它被建立開始,一直到它最後一次被使用結束。
let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{} and {}", r1, r2); // r1和r2此後沒有再被使用 let r3 = &mut s; // no problem println!("{}", r3);
r1
、r2
與r3
的做用域沒有交叉,因此能夠經過編譯。
在其餘的一些帶有指針的語言中,很容易不當心建立一個dangling pointer,一個指向 某段已經被someone else使用的內存 的引用。在Rust中,編譯器會保證在引用脫離做用域前,被引用的數據不會脫離做用域。如如下一段代碼會被拋出CE:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
解決方法是直接返回s
,而不是它的引用。
另外一種沒有ownership的數據類型是slice,切片可讓恁引用數據的某一段連續的部分而不是整個數據。
好比返回一個字符串的第一個單詞的函數:
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() }
由於咱們要在String
中一位一位地查找空格,咱們將String
用as_bytes
方法轉換爲一列字節。不然將會拋出CE。
.as_bytes()
→ as bytes.iter().enumerate()
→ 將.iter()
的結果用一個tuple(index, reference)
包起來返回b''
→ byte literal syntax,將字符byte化可是若是是要查找第二個單詞呢?若是在查找完以後字符串又發生了改變呢?
String slice是對String
的一個部分的引用,如:
let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11];
咱們能夠用[starting_index..ending_index]
(左閉右開)建立切片,也就是說切片的數據結構存儲了切片開始的位置與切片長度(length == ending_index - starting_index)。
其中&s[0..5]
能夠縮寫爲&s[..5]
。此外,&s[3..len]
能夠縮寫爲&s[3..]
,&s[0..len]
能夠縮寫爲&s[..]
。
運用切片重寫上面的函數:
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] }
因爲返回的實際上是一個s
的不可變引用,所以在後續操做裏不會有咱們上面所擔憂的錯誤。
回憶這句代碼:
let s = "Hello, world!";
這裏的s
的類型實際上是&str
,實際上是一個切片,這也就是字符串不可變的緣由——&str
是不可變引用。(注意:&str
!=&String
)
在知道了咱們能夠對字符串和String
取切片後,咱們能夠將下面的代碼進行改進:
fn first_word(s: &String) -> &str {
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
fn first_word(s: &str) -> &str {
若是咱們要傳遞的是一個字符串切片,咱們能夠直接傳遞它。若是咱們要傳遞的是一個String
,咱們能夠傳遞整個String
做爲切片[..]
。這樣改進可使咱們的API更通用。
還有其它更通用的切片類型,如:
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3];
這個切片的類型是&[i32]
,機制和字符串切片同樣——存儲起始點和長度。
ownership、借用和切片的概念保證了內存的安全。
The Rust Programming Language by Steve Klabnik and Carol Nichols, with contributions from the Rust Community : https://doc.rust-lang.org/book/