Rust 入門 (四)

全部權是 rust 語言獨有的特性,它保證了在沒有垃圾回收機制下的內存安全,因此理解 rust 的全部權是頗有必要的。接下來,咱們來討論全部權和它的幾個特性:借用、切片和內存結構。編程

什麼是全部權

Rust 的核心特性是全部權。各類語言都有它們本身管理內存的方式,有些是使用垃圾回收機制,有些是手動管理內存,而 rust 使用的是全部權機制來管理內存。數組

全部權規則

全部權規則以下:安全

  • rust 中的每一個值都有一個本身的變量。
  • rust 值在同一時間只能綁定一個變量。
  • 變量超出做用域,值會自動被銷燬。

不懂不要緊,跳過日後看。編程語言

變量做用域

rust 語言的變量做用域和其餘語言是相似的,看例子:函數

{                      // 變量 s 尚未被聲明,s 在這裏是無效的
    let s = "hello";   // 變量 s 是這裏聲明的,從這裏開始生效

    // 從這裏開始,可使用 s 作一些工做
}                      // 變量 s 超出做用域,s 從這裏開始再也不生效

能夠總結兩點重要特性:學習

  • 當變量 s 聲明以後開始生效
  • 當變量 s 出了做用域失效

String 類型

在章節三中學習的數據類型都是存儲在內存的棧空間中,當它們的做用域結束時清空棧空間,咱們如今學習一下內存的堆空間中存儲的數據是在什麼時候被 rust 清空的。編碼


咱們在這裏使用 String 類型做爲例子,固然只是簡單的使用,具體的內容後文介紹。操作系統

let s = "hello";

這個例子是把 hello 字符串硬編碼到程序中,咱們把它叫作 字符串文字 (string literals 我不知作別人是怎麼翻譯的,我實在想不到合適的詞,先這樣叫着吧),字符串文字很方便,可是它不能適用於任何場景,好比咱們想要輸入一個文本串的時候,原理以下:翻譯

  • 字符串文字是不可修改的
  • 編碼(編譯)時不肯定文本串的內容 (好比保存用戶的輸入)
    在這種狀況下,咱們會使用字符串的第二種類型——String。它是存儲在堆內存中的,並且容許在編譯期間不知道字符串的大小,咱們先使用 from 函數從字符串文字中建立一個 String 類型的字符串。
let s = String::from("hello");

這種雙冒號的語法細節下個章節再說,這裏先聚焦於字符串,例子中建立的字符串是能夠改變的,好比:指針

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在上個字符串後追加一個字符串

println!("{}", s); // 這裏會打印 `hello, world!`

內存分配

字符串文字是在編譯期間就有肯定的字符內容,因此文本能夠直接硬編碼到程序中,這就是字符串文字快捷方便的緣由。可是字符串文字又是不可變的,咱們沒辦法分配內存給編譯期間未知大小及變化的字符串。



字符串類型則支持字符串的修改和增加,即便編譯期間未知大小,也能夠在堆內存分配一塊空間用於存儲數據,這意味着:

  • 內存必須是在運行時,從操做系統中請求 (和大多數編程語言相似,當咱們調用 String::from 方法時,就完成了對內存的請求)
  • 當不使用這塊內存時,咱們纔會把它返還給操做系統 (和其它語言不一樣,其它語言是使用垃圾回收機制或手動釋放內存)

rust 則是另外一種方式:一旦變量超出做用域,程序自動返還內存,經過下面的例子來看這個概念:

{
    let s = String::from("hello"); // s 從這裏開始生效

    // 利用 s 作一些事情
}                                  // s 超出做用域,再也不生效

當 s 超出做用域,rust 會自動幫咱們調用一個特殊的函數—— drop 函數,它是用於返還內存的,當程序執行到 大括號右半塊 } 的時候自動調用該函數。

變量和數據交互方式:移動

在 rust 中,可使用不一樣的方式在多個變量間交互相同的數據,好比:

let x = 5; // 把 5 綁定到 x 上
let y = x; // 把 x 的值複製給 y,此時,x 和 y 的值都是 5

再好比:

let s1 = String::from("hello"); // 建立一個字符串綁定到 s1 上
let s2 = s1;            // 把 s1 的值移動給 s2,此時,s1 就失效了,只有 s2 是有效的

s1 爲何失效,由於字符串是存儲在堆內存中的,這裏只是把棧內存中的 s1 的數據移動給 s2,堆內存不變,這種方式叫作淺克隆,也叫移動。若是想讓 s1 仍然有效,可使用深克隆。

變量和數據交互方式:克隆

關於深克隆,咱們直接看例子吧:

let s1 = String::from("hello"); // 建立一個字符串綁定到 s1 上
let s2 = s1.clone();        // 把 s1 的值克隆給 s2,此時,s1 和 s2 都是有效的

println!("s1 = {}, s2 = {}", s1, s2); // 打印 s1 和 s2

下一章節再詳細介紹這種語法。

只有棧數據:複製

咱們再回到前面的例子中:

let x = 5; // 把 5 綁定到 x 上
let y = x; // 把 x 的值複製給 y,此時,x 和 y 的值都是 5

println!("x = {}, y = {}", x, y); // 打印 x 和 y

這裏沒有使用 clone 這個方法,可是 x 和 y 都是有效的。由於 x 和 y 都是整型,整型存儲在棧內存中,即便調用了 clone 方法,也是作相同的事。下面總結一下複製的類型:

  • 全部的整型,像 u32,i32
  • 布爾類型,像 bool,值是 true, false
  • 全部的浮點型,像 f32,f64
  • 字符類型,像 char
  • 元組,可是僅僅是包含前 4 種類型的元組,像 (u32, i32),可是 (u32, String) 就不是了

全部權和函數

這裏直接放一個例子應該就說清楚了,以下:

fn main() {
    let s = String::from("hello");  // s 進入做用域

    takes_ownership(s);             // s 移動到函數裏

    // s 從這裏開始再也不生效,若是還使用 s,則會在編譯期報錯

    let x = 5;                      // x 進入做用域

    makes_copy(x);                  // x 複製(移動)到函數裏,
                                    // 但因爲 x 是 i32 ,屬於整型,仍有效
                                    // 使用 x 作一些事情

} // x 超出做用域,因爲 s 被移動到了函數中,這裏再也不釋放 s 的內存

fn takes_ownership(some_string: String) { // some_string 進入做用域
    println!("{}", some_string);

} // some_string 超出做用域,而後調用 drop 函數釋放堆內存

fn makes_copy(some_integer: i32) { // some_integer 進入做用域
    println!("{}", some_integer);

} // some_integer 超出做用域,可是整型不須要釋放堆內存

返回值和做用域

這塊也直接放個例子,以下:

fn main() {
    let s1 = gives_ownership();         // gives_ownership 把它的返回值移動給 s1
    let s2 = String::from("hello");     // s2 進入做用域

    let s3 = takes_and_gives_back(s2);  // s2 移動進函數,函數返回值移動給 s3

} // s3 超出做用域被刪除,s2 超出做用域被刪除,s1 超出做用域被刪除

fn gives_ownership() -> String {             // gives_ownership 將移動它的返回值給調用者

    let some_string = String::from("hello"); // some_string 進入做用域

    some_string                              // some_string 是返回值,移出調用函數
}

// takes_and_gives_back 移入一個字符串,移出一個字符串
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入做用域

    a_string  // a_string 是返回值,移出調用函數
}

引用和借用

前面都是把值傳入傳出函數,這裏咱們學習一下引用,看個例子:

fn main() {
    let s1 = String::from("hello");  // 建立一個字符串 s1

    let len = calculate_length(&s1); // 把 字符串 s1 的引用傳給函數

    println!("The length of '{}' is {}.", s1, len); // 這裏能夠繼續使用 s1
}

fn calculate_length(s: &String) -> usize { // 接收到 字符串 s1 的引用 s
    s.len()  // 這裏返回函數的長度
} // s 超出做用域,可是這裏沒有字符串的全部權,不釋放內存

&s1 語法是建立一個指向 s1 的值的引用,而不是 s1 自己,當引用超出做用域不會釋放引用指向值的內存。被調用函數聲明參數的時候,參數的類型也須要使用 & 來告知函數接收的參數是個引用。

修改引用

在上述例子中,若是在 calculate_length 函數中修改字符串的內容,編譯器會報錯,由於傳入的引用在默認狀況下是不可變引用,若是想要修改引用的內容,須要添加關鍵字 mut,看例子:

fn main() {
    let mut s = String::from("hello");

    change(&mut s); // mut 贊成函數修改 s 的值
}

fn change(some_string: &mut String) { // mut 聲明函數須要修改 some_string 的值
    some_string.push_str(", world");    // 追加字符串
}

可變引用有一個很大的限制:在特定做用域中針對同一引用,只能有一個可變引用。好比:

let mut s = String::from("hello");

let r1 = &mut s; // 這是第一個借用的可變引用
let r2 = &mut s; // 這是第二個借用的可變引用,這裏會編譯不經過

println!("{}, {}", r1, r2);

這個限制的好處是編譯器能夠編譯期間阻止數據競爭,數據競爭發生在以下狀況:

  • 兩個或多個指針同時訪問相同的數據
  • 多個指針在寫同一份數據
  • 沒有同步數據的機制

數據競爭會形成不可預知的錯誤,並且在運行時修復是很困難的,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; // 不可變引用,沒問題
let r2 = &s; // 不可變引用,沒問題
let r3 = &mut s; // 可變引用,會發生大問題,由於後面還在使用 r1 和 r2

println!("{}, {}, and {}", r1, r2, r3);

當存在不可變引用時,就不能再借用可變引用了。不可變引用不會修改引用的值,因此能夠借用多個不可變引用。可是若是不可變引用都不使用了,就又能夠借用可變引用了,好比:

let mut s = String::from("hello");

let r1 = &s; // 不可變引用,沒問題
let r2 = &s; // 不可變引用,沒問題
println!("{} and {}", r1, r2);
// r1 和 r2 從這裏開始再也不使用了

let r3 = &mut s; // 可變引用,也沒問題了,由於後面沒使用 r1 和 r2 的了
println!("{}", r3);

懸空引用

在一些指針語言中,很容易就會錯誤地建立懸空指針。懸空指針就是過早地釋放了指針指向的內存,也就是說,堆內存已經釋放了,而指針還指向這塊堆內存。在 rust 中,編譯器能夠保證不會產生懸空引用:若是有引用指向數據,編譯器會確保引用指向的數據不會超出做用域,好比:

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

fn dangle() -> &String { // 這裏但願返回字條串的引用
    let s = String::from("hello"); // 建立字符串

    &s // 這裏返回了字符串的引用

} // 這裏會把字符串的內存釋放,由於 s 在這裏超出做用域

這個函數作個簡單的修改就行了,看以下例子:

fn no_dangle() -> String { // 不返回字符串引用了,直接返回字符串
    let s = String::from("hello");

    s // 返回字符串
}

引用規則

引用的規則以下:

  • 任什麼時候候,只能有一個可變引用或多個不可變引用
  • 引用必須老是有效的

切片類型

另外一個沒有全部權的數據類型是切片。切片是集合中相鄰的一系列元素,而不是整個集合。


作個小小的編程題目:有一個函數,輸入一個英文字符串,返回第一個單詞。若是字符串沒有空格,則認爲整個字符串是一個單詞,返回整個字符串。


咱們來思考一下這個函數結構:

fn first_word(s: &String) -> ?   // 這裏應該返回什麼

在這個函數中,字符串引用做參數,函數沒有字符串的全部權,那咱們應該返回什麼呢?咱們不能返回字符串的一部分,那麼,咱們能夠返回第一個單詞結束位置的索引。看實現:

fn first_word(s: &String) -> usize { // 返回一個無符號整型,由於索引不會小於 0

    let bytes = s.as_bytes(); // 把字符串轉換化字節類型的數組

    // iter 方法用於遍歷字節數組,enumerate 方法用於返回一個元組,元組的第 0 個元素是索引,第 1 個元素是字節數組的元素
    for (i, &item) in bytes.iter().enumerate() {

        if item == b' ' { // 若是找到了空格,就返回對應的索引
            return i;
        }
    }

    s.len() // 若是沒找到空格,就返回字符串的長度
}

如今,咱們找到了返回字符串第一個單詞的末尾索引,可是還有一個問題:函數返回的是一個無符號整型,返回值只是字符串中的一個有意義的數字。換句話說,返回值和字符串是分開的值,不能保證它永遠有意義,好比:

fn main() {
    let mut s = String::from("hello world"); // 建立字符串

    let word = first_word(&s); // word 被賦值爲 5

    s.clear(); // 清空字符串,如今字符串的值是 ""

    // word 還是5,可是咱們不能獲得單詞 hello 了,這裏使用 word,編譯器也不會報錯,可是這真的是一個 bug
}

還有一個問題,若是咱們想獲得第二個單詞,應該怎麼辦?函數聲明應該是:

fn second_word(s: &String) -> (usize, usize) {

若是想獲得不少個單詞,又應該怎麼辦?

字符串切片

字符串切片就是字符串一部分的引用,以下:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

就是取字符串 s 中的一部分組成一個新的變量,取值區間左閉右開,就是說,包括左邊的索引,不包括右邊的索引。


若是左邊索引是0,可省略:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2]; // 和上一行等價

若是右邊索引是字符串末尾,可省略:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..]; // 和上一行等價

若是取整個字符串,能夠把兩邊都省略:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &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[..]
}

這樣就直接返回了第一個單詞的內容。若是要返回第二個單詞,可寫成:

fn second_word(s: &String) -> &str {

咱們再來看一下字符串清空的問題是否還存在:

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // 不可變借用在這裏聲明

    s.clear(); // 這裏會報錯,由於這裏在修改 s 的內容

    println!("the first word is: {}", word); // 不可變借用在這裏使用
}

不可變借用的聲明和使用之間是不能使用可變借用的。

字符串文字是切片

前面咱們討論過字符串文字的存儲問題,如今咱們學習了切片,咱們能夠理解字符串文字了:

let s = "Hello, world!";

s 的類型是 &str,這是一個指向了二進制程序特殊的位置的切片,這就是字符串文字不可變的原是,&str 是一個不可變引用

字符串切片做爲參數

前面學習了字符串文字切片和字符串類型切片,咱們來提升 first_word 函數的質量。有經驗的 rust 開發者會把函數參數類型寫成 &str,由於這樣可使得 &String 和 &str 使用相同的函數。好像不太好理解,直接上例子:

fn main() {
    let my_string = String::from("hello world"); // 建立字符串

    // first_word 使用 &String切片
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world"; // 建立字符串

    // first_word 使用 &str切片
    let word = first_word(&my_string_literal[..]);

    // 由於字符串文字已是字符串切片了,能夠不使用切片語法
    let word = first_word(my_string_literal);
}

其它切片

咱們只舉一個 i32 類型切片的例子吧:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];  // 值是: [2, 3]

rust 使用了全部權、借用、切片,在編譯期確保程序的內存安全。rust 語言提供了和其餘編程語言相同的方式來控制內存,而不須要咱們編寫額外代碼來手動管理內存,當數據超出全部者的做用域就會被自動清理。

歡迎閱讀單鵬飛的學習筆記

相關文章
相關標籤/搜索