RUST 0x03 Ownership

RUST 0x03 Ownership

Ownership是Rust最獨特的一個特性,可以保證內存安全。數組

1 What's the Ownership!?

Ownership Rules

  • Rust中的每個值都有一個叫作它的owner的變量。
  • 同時只能有一個owner。
  • 當owner離開做用域(scope),這個值將被丟棄。

變量做用域

以string爲例:安全

{                      // s尚未被聲明,不有效
    let s = "hello";   // 從這裏開始,s有效
    // 用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)。

變量和數據interact的方法:Move

整數例:

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會自動歸還其內存。可是這裏咱們的s1s2都指向同一個內存,那麼這會不會引發double free錯誤呢?

爲了保證內存安全,在進行let s2 = s1;以後,Rust就認爲s1再也不有效,所以當s1脫離做用域時也沒必要歸還內存。如:

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

編譯時將會拋出一個CE。

由於在複製指針、長度、容量以後,s1再也不有效,因此咱們能夠說是s1被移動(move)到了s2

變量和數據interact的方法:Clone

若是咱們想要複製的是String在堆中的數據,而不只僅是棧中的數據,咱們能夠用一個經常使用方法clone。如:

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

此時s2s1的一份複製,包括棧中的數據與堆中的數據,此時s1仍然有效。可是要付出運行效率的代價。

Stack-Only Data: Copy

如下的一段代碼可以運行,並且有效:

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,包括truefalse
  • 全部浮點類型,好比f64
  • 字符類型char
  • 只包含可Copy類型的Tuple,好比(i32, i32)Copy,而(i32, String)不能夠。
  • 數組與Tuple同理

Ownership與函數

將一個值傳遞給一個函數的原理與將一個值賦值給一個變量是類似的,要麼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)
}

但這太麻煩了……

2 引用與借用(References&Borrowing)

利用引用能夠方便地讓函數在不取走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

  • 兩個以上的指針同時指向同一個數據。
  • 至少一個指針在被用來對數據進行寫入。
  • 沒有同時access數據的機制。

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);

r1r2r3的做用域沒有交叉,因此能夠經過編譯。

Dangling References

在其餘的一些帶有指針的語言中,很容易不當心建立一個dangling pointer,一個指向 某段已經被someone else使用的內存 的引用。在Rust中,編譯器會保證在引用脫離做用域前,被引用的數據不會脫離做用域。如如下一段代碼會被拋出CE:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

解決方法是直接返回s,而不是它的引用。

引用的規則(The Rules of References)

  • 在任一時間,同時只能有一個可變引用或者多個不可變引用。
  • 引用必須是有效的。

3 切片(Slice)類型

另外一種沒有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中一位一位地查找空格,咱們將Stringas_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/

相關文章
相關標籤/搜索