1. 程式人生 > 其它 ><<Rust程式設計語言>>個人版(4: 所有權)

<<Rust程式設計語言>>個人版(4: 所有權)

認識所有權

什麼是所有權

rust的核心之一就是所有權

對於執行中的程式來說, 他必須對使用的記憶體進行管理, 同時對於執行中產生的垃圾, 程式也需要進行銷燬, 以免出現記憶體洩露等問題

某些語言自帶了垃圾回收機制, 在程式執行時不斷的掃描尋找不再使用的記憶體將其釋放

另一些語言要求程式設計師通過程式碼來自己分配和釋放記憶體

而rust使用的是第三種, 通過所有權系統來管理記憶體, 編譯器在編譯時會根據規則來進行檢查. 在執行時可以保證不會減慢程式的執行速度

棧(stack)與堆(heap)

棧和堆都是在程式執行中可供使用的記憶體, 他們的結構並不相同, 棧是有序的, 他就像一個水桶, 最上面的是棧頂, 向這個棧裡存放資料叫做進棧

, 他像一個水桶, 所以儲存資料只能存放在水桶的最上面, 當移除資料(出棧)的時候, 也只能從棧頂移除, 所以, 棧遵循先進後出的邏輯.

棧中的所有資料都必須佔用已知且固定的大小, 而且棧中的資料是有大小限制的, 所以在程式執行中出現的大小未知或者可能變化的資料, 必須儲存在堆上, 堆不是有序的, 當你向堆中儲存一個數據, 作業系統首先在堆的某處找到一塊足夠大的空間, 把它標記為已使用, 然後回傳該空間的指標, 這個過程叫做在堆上分配記憶體, 指標的大小是已知且固定的

所以可以把真正的資料儲存在堆中, 將指標存放在棧中, 當需要訪問真實資料時, 先獲取指標, 再訪問指標

入棧比在堆上分配記憶體快, 這是因為棧在建立時每一塊資料的大小是固定的, 而且是有序的, 作業系統無需為新資料去搜索合適的記憶體空間. 當在堆上分配記憶體時, 系統需要先找到一塊足夠大的記憶體, 然後做記錄

訪問堆上的資料也比訪問棧的資料慢, 堆上面的資料通過指標訪問, 現代處理器在記憶體中跳轉越少速度就越快(快取), 而堆是無序的, 意味著指標指向的地方可能需要很多次記憶體跳轉

同樣的, 因為這個原因, 處理器在處理資料彼此相近的時候(比如棧)比遠的時候(堆)效率更高. 在堆上分配大量空間也會消耗時間.

當代碼呼叫一個函式時, 會將函式的值和函式內部的區域性變數壓入棧中, 當這個函式結束時, 這些資料就屬於垃圾, 理應被回收, 此時則出棧, 因為棧是後進先出, 導致這種回收是快速的, 符合邏輯的, rust的所有權就是這樣做的

跟蹤哪部分程式碼正在使用哪些資料, 最大限度的減少堆上重複資料的數量, 同時清理堆上不再使用的資料, 這些就是所有權系統需要去關心的

// 假設一個抽象的棧 []

{
  A;  // A入棧, [A]
  B;  // B入棧, [B, A]
  {
    a;  // a入棧, [a, B, A]
    b;  // b入棧, [b, a, B, A]
  };
  // 函式結束了, a和b是垃圾了, 將a和b出棧, 直接取棧頂的的一段即可,保證效率的同時也符合邏輯(程式碼從上往下執行的順序), [b, a, B, A] -> [B, A]
  C;  // C入棧, [C, B, A]
  D;  // D入棧, [D, C, B, A]
}

所有權的基本規則

Rust中的每一個值都有且只有一個被稱為其 所有者 的變數

值在任一時刻有且只有一個所有者

所有者離開作用域時, 這個值將會被丟棄

變數作用域

每個變數都有其作用域(scope), 作用域是一個項(item)在程式中有效的範圍

let s="hello";

這裡的變數s繫結到了字串hello中, 這個字串編碼進了程式程式碼中, 那麼s從宣告開始到當前作用域結束時都是有效的

fn main() {
    // s未宣告
    let s = "hello";  // s在這裡生效
    // 可以使用s
}// 函式結束, 作用域也結束, s無法使用了

這裡有兩個關鍵點

s進入作用域時, s是有效的

s離開作用域, s無效

String型別

上面的例子來說, s因為是不可變的, 加上其資料很小, 所以本體儲存在棧中

本次測試將把資料的本體放置在堆中, 而將指標放置在棧中

我們這裡使用String作為例子, 專注於String與所有權相關部分.

對於在編譯時無法知道具體的值的變數, 也就是說並不知道大小, 他就會被分配到堆上, 比如String

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

這裡的::是運算子, 具體的詳情我們在之後的章節說明.

fn main() {
    let mut s = String::from("hello");
    s.push_str(", world");  // 在s後拼接字串
    println!("{}", s);  // hello, world
}

這裡得到的s是可變的, 他可以通過呼叫 push_str 函式來修改自己

記憶體與分配

對於字串字面值來說, 我們在編譯時就能準確的知道其內容, 所以直接硬編碼進最終的可執行檔案中, 這使得字串字面值快速且高效. 這裡的前提是字串字面值的不可變性, 但是, 對於未知的文字, 我們無法在開始時確定大小, 因為他是可以改變的.

對於string型別, 為了讓他可以支援一個可變的, 可增長的文字片段, 需要在堆上分配一塊在編譯時未知大小的記憶體存放資料, 這就有兩個問題

  • 必須在執行時向作業系統請求記憶體
  • string處理完成後將記憶體返回給作業系統

如何在執行時請求記憶體呢? 當我們呼叫String::from 時, 他會請求所需要的記憶體

如何在處理完成後將記憶體返給作業系統呢? 在有垃圾回收GC的語言中, GC會記錄和清理不再使用的記憶體, 作為開發者我們不需要關心他, 沒有GC則需要開發者手動的釋放, 就跟請求一樣需要我們寫在程式碼中, 正確的處理記憶體回收通常比較困難, 如果忘記回收會浪費記憶體, 導致記憶體洩露等. 如果回收過早, 可能會在後續的使用中出現無效變數, 如果重複回收也可能會導致問題, 所以要準確的在合適的地方對一個分配(allocate)配對一個釋放(free)

在Rust中, 記憶體在擁有他的變數離開作用域時就被自動釋放, 例如下面的例子

{
    let mut s = String::from("hello");  // 建立s, s此時是有效的, 在此作用域中
    s.push_str(", world");  // 使用修改s, 在s後拼接字串
    println!("{}", s);  // hello, world
}  // 該作用域已結束, 作用域內的使用的記憶體需要釋放
// s失效了

這裡, 當s離開當前有效的作用域時, Rust為我們自動呼叫函式drop (前提是該資料型別有drop), 該函式可以將變數釋放, Rust會在結尾}自動呼叫 需要釋放的變數的drop

變數與資料互動: 移動

如果我們將一個變數賦值給另一個變數, 其資料會怎麼處理呢?

    let x = 5;  // x為5
    let y = x;  // copy x的值5, 賦值給y
    // x和y都等於5

因為5是在編譯時就可以確定的, 所以這兩個5被放入了棧中

那麼對於無法確定大小的變數來說, 例如

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

之前說過, 對於不可預測的變數, 我們會在棧上儲存指標而在堆上儲存真正的資料, 那麼對於s1來說, 在棧上的資料為

name value
ptr(指標) 堆上的地址
len(長度) 2
capacity(容量) 2

指標指向的堆的資料為

index value
0 s
1 1

這就是將s1繫結給變數s1在記憶體中的表現形式

我們注意到, 棧上的資料有 長度 和 容量, 長度指的是當前使用了多少位元組的記憶體, 容量指的是從作業系統申請了多少位元組的記憶體, 這兩個是不一樣的, 不要混淆

而我們將s1賦值給s2時, 實際上只是從拷貝了s1在棧上的資料, 也就是說此時 s1與s2共同指向了一個堆地址, 這跟其他語言的淺COPY(shallow copy)非常像, 這樣做的好處是使操作變得快速, 如果是深COPY(deep copy), 意味著需要將堆上的資料找到,再插入到堆的另一個地方, 如果堆上的值很大, 則會造成效率的低下

但是這樣會導致問題出現, 例如當s1與s2離開了作用域時, Rust 會對s1和s2進行清理, 但是他們實際上指向了同一個地址, 兩次清理一個記憶體, 這就會出現前文提到的二次釋放(double free)問題, 因此, Rust使用了不同的方法, 即移動(move), 就是在執行 s2=s1 時, 將s1無效化

fn main() {
    let s1 = String::from("s1");  // 建立s1
    let s2 = s1;  // 將s1棧上的資料轉移到s2上, s1失效了
    println!("{}", s1);  // s1不可用, 所以會出錯
    println!("{}", s2);  // s2可用
}  // drop時, 先處理s2的Drop再s2的棧, s1無了, 只清理s1的棧

執行該程式碼時, 會出錯

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:20
  |
2 |     let s1 = String::from("s1");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership`.

Rust禁止你使用無效的引用

所以, Rust在處理這個s2 = s1時, 先將s1的棧資料複製一份給s2, 然後將s1置空, 這樣就解決了二次釋放的問題

需要知道的是, Rust的設計原則是: 永遠不會自動建立資料的深拷貝, 這是出於對效能影響的考慮

變數與資料互動: 克隆

如果你確實需要進行深copy, 你可以使用內建函式clone

fn main() {
    let s1 = String::from("s1");  // 建立s1, s1進入作用域
    let s2 = s1.clone();  // s1的堆資料複製為另一份, 然後重新生成棧資料, 指向新的堆資料, s2進入作用域
    println!("{}", s1);  // 未發生轉移, s1還是可用的
    println!("{}", s2);  // s2可用
}  // drop時, s1和s2的堆資料並不是一個, 所以沒有二次釋放的問題, 先進後出所以先清理s2

必須要注意的是, 這樣會對資源和效能造成一定的損耗, 在確保你必須這樣做時才需要進行克隆操作

只在棧上的資料: 拷貝

而對於只在棧上儲存的資料, 也就是在編譯時就知道值的資料來說, 不存在轉移和克隆, 因為他是隻儲存在棧上, 所以進行拷貝速度很快,Rust在處理這種資料的賦值時直接copy棧的資料到另一個變數, 所以兩個變數都可用

fn main() {
    let s1 = 1;  // 建立s1, 進入作用域
    let s2 = s1;  // 拷貝s1的棧資料生成s2, s2進入作用域
    println!("{},{}", s1, s2)  // s1與s2都可以使用
}  // i32沒有drop, 根據先進先出, 清理s2再s1

怎麼分辨什麼是會出現移動的呢, Rust有一個叫做Copy的trait的特殊註解, 如果某個型別擁有這個註解, 那麼舊的變數在賦值給新的變數後依舊可用. 如果一個型別有Drop註解, 那麼他就無法使用Copy註解. 他們是無法共存的.

什麼型別是Copy的呢? 可以檢視對應的文件. 一般的, 任何簡單標量值的組合可以是Copy的, 不需要分配記憶體或者某種形式的資源型別是Copy的, 比如

  • 所有整數型別
  • 布林型別
  • 所有浮點型別
  • 字元型別, char
  • 元組, 當其包含的型別都是Copy

drop與記憶體釋放的關係

這裡是本人記錄的

需要注意的是, Rust釋放記憶體有兩種

如果是有drop註解的資料型別(例如String), 先執行drop方法,再將棧資料刪除

而沒有drop註解的資料型別(例如i32), 直接將棧資料刪除

為什麼drop與copy註解不能相容

這裡是本人記錄的

我們知道, copy註解代表著該資料型別並不會發生轉移, 也就是說發生 s1 = s2 時,s2 依舊存在, 在內部邏輯中是Copy一份棧資料, 有copy 的資料型別一般只將資料放置在棧上, 在退出作用域時, 只需要清理棧資料即可, 而擁有drop的資料型別, rust會優先呼叫drop方法, 一般來講, drop 一般是清理堆的有關資料, copy的不需要清理, 所以為了保持統一, 就規定了兩者不相容

如果某個型別同時擁有CopyDrop註解的話, 首先擁有Drop一般都需要將資料本體放置進堆, 那樣在重複賦值時又有Copy會Copy一份棧資料, 就造成了兩個變數實際上指向了同一個資源, 在清理時就會發生二次釋放的問題

所有權與函式

fn main() {
    let s = String::from("s");  // 建立s, 進入作用域
    takes_ownership(s);  // s轉移進了函式takes_ownership的some_string中
    println!("{}", s);  // 這裡會報錯, 因為s已經轉移, s不可用了

    let i = 5;  // i進入作用域
    makes_copy(i);
    println!("{}", i);  // i可用, 因為i是儲存在棧上, 有`Copy`直接複製一份進makes_copy的some_integer
}  // s和i退出作用域, 棧是先進後出, 所以先清理i, i沒有`Drop`所以直接刪除棧, 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 釋放, 因為i32沒有`drop`註解所以只清理棧資料

返回值與作用域

如果函式是擁有返回值, 如果返回值是drop, 則會將返回值移動給返回值的接收者, 如果是copy, 則是copy給返回值

變數的所有權總是遵循相同的模式: 將值付給另一個變數時移動他. 當持有堆中資料值的變數離開作用域時, 堆中值會通過drop被清理, 除非資料被移動到另一個變數

看下面程式碼

fn main() {
    let s1 = gives_ownership();  // s1接受返回值, 進入作用域

    let s2 = String::from("s2");  // 建立s2, 進入作用域

    let s3 = takes_and_gives_back(s2);  // s2傳入takes_and_gives_back函式, s2轉移給該函式的a_string, 隨後s3接受返回值, 進入作用域

    println!("{}", s2);  // 這裡s2已經發生轉移, 指標為空, 所以使用會報錯
    println!("{}, {}", s1, s3);  // s1和s3可用
} // 退出作用域, 清理s3>s2>s1
// s2為空


fn gives_ownership() -> String { 
    let some_string = String::from("gives_ownership");

    some_string  // 如果這裡返回了, 而返回值是String型別, 有Drop註解, 會發生所有權的移動, 移動給接受者, some_string失效了
}  // some_string 移除作用域, 因為 some_string 已經轉移所以只刪除棧

fn takes_and_gives_back(a_string: String) -> String {

    a_string  // 如果這裡返回了, 而返回值是String型別, 有Drop註解, 會發生移動, 移動給接受者, a_string失效了
}  // a_string 移除作用域, 因為 a_string 已經轉移所以只刪除棧

這樣就會出現一個問題, 如果某個變數是擁有Drop的, 那麼這個變數需要作為某個函式的引數使用, 我們傳入到這個函式中總會使原有的變數失效, 那麼如果我還需要再使用這個變數呢?

有一個折中的辦法, 在函式中再將引數值傳出, 例如

fn main() {
    let s1 = String::from("s1"); 
    let (s2, len) = calculate_length(s1);  // s1轉移了, 用s2接受原來的s1

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字串的長度

    (s, length)  // 將s也返回
}

但是這樣太LOW了, 為了解決這樣的問題, Rust提供了引用(references)

引用和借用

引用可以在不轉移所有權的情況下使用變數

fn main() {
    let s1 = String::from("hello");  // s1進入作用域
    let len = calculate_length(&s1);  // 將 &s1 傳入, &意思是引用, 即將s1的引用傳入函式calculate_length

    println!("The length of '{}' is {}.", s1, len);  // 仍可以使用s1, 因為未發生所有權轉移
}

fn calculate_length(s: &String) -> usize {  // 因為型別變成了String的引用, 所以接收引數型別發生變化
    s.len()
}

&代表對某個引用, 引用允許你使用值但不獲取其所有權, 比如上面的 s1, 當 s1傳入到 calculate_length 的引數 s 時, 實際上s是 s1 的引用, 類似於指標, 指向了s1

與&(引用)相反的操作是解引用(dereferences), 他的運算子是 *, 之後會講到

&s1 讓我們建立一個指向s1的引用, 但是並不擁有他, 因為不擁有他, 所以當引用離開作用域時其指向的值也不會被清理

在 calculate_length 結束時, s離開作用域, 理應清理, 但是因為s只是個引用型別, 所以只把s清理並不會清理s對應的真正的變數

對於函式 calculate_length 來說, 其接受了String的引用, 這種行為被稱為 借用

需要注意的是, 如果你借用了某個變數, 那你 預設情況下 是無法修改這個變數的值的

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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");  // 嘗試追加字串
}

會報錯

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
8 |     some_string.push_str(", world");  // 嘗試追加字串
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership`.

提示你無法修改他, 當然這是預設情況下

可變引用

某些情況下可以修改引用的值, 我們修改程式碼成

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

    change(&mut s);  // &mut 表示是可變的引用
    println!("{}", s)  // hello, world
}

fn change(some_string: &mut String) {  // 同樣的引數型別也要 mut
    some_string.push_str(", world");  // 嘗試追加字串
}

這樣即可執行, 但是注意, 可變引用有幾個限制

在特定作用域的特定資料只能有一個可變引用

例如以下程式碼

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;  // 錯誤, 因為s的 &mut 同時只能出現一個
    println!("{},{}", r1, r2);
}

會報錯

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:4:14
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
5 |     println!("{},{}", r1, r2)
  |                       -- first borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership`.

這是為了避免出現資料競爭的問題, 資料競爭通常由這三種行為造成:

  • 兩個或多個指標同時訪問一個數據
  • 至少一個指標寫入資料
  • 沒有同步資料的機制

資料競爭可能導致出現BUG, 並且讓開發者難以定位和解決問題, 所以Rust在編譯時會檢查這個問題

當然, 這個限制只是存在於同一個作用域, 例如下面的程式碼是可以的

fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
    } // 可變引用r1退出作用域
    let r2 = &mut s;  // 可以重新建立
}

在特定作用域的特定資料不能同時擁有可變和不可變引用

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

    let r1 = &s; // 不可變1
    let r2 = &s; // 不可變2
    let r3 = &mut s; // 可變1
    
    println!("{}, {}, and {}", r1, r2, r3);  // 會報錯, 因為不可變與可變引用無法共存
    
}

報錯

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // 不可變1
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // 不可變2
6 |     let r3 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
7 |     
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.

兩者不能共存, Rust認為如果你使用了不可變引用. 你一定不希望他在某些時候變化, 所以禁止共存, 但是對於多個不可變引用, 是可以的.

因為都是讀取, 就是安全的, 沒有辦法影響到別人, 所以可以一個作用域可以有多個不可變引用存在

標題所言的是特定作用域, 對於引用來說, 他的作用域從宣告的地方開始到最後一次使用為止. 如果宣告未使用, 那麼只存在於宣告的那一行, 當然最好不要宣告卻不使用, 這是不好的習慣

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

    let r1 = &s; // 不可變1, 未使用
    let r2 = &s; // 不可變2, 未使用
    let r3 = &mut s;  // 可變1, 使用
    
    println!("{}", r3);  // 這裡已經超出了r1和r2的作用域, 因為r1/r2未使用, 作用域只有生成的一行
}
fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // 不可變1, 未使用
    let r2 = &s; // 不可變2, 未使用
    println!("{},{}", r1, r2);  // 這裡是r1/r2最後一次使用, r1/r2作用域到此結束

    let r3 = &mut s;  // 可變1, 使用
    println!("{}", r3);  // 可使用, 當前作用域無不可變引用
}

垂懸引用

垂懸指標指的是指標指向的內容已經被分配給了其他的持有者.

在Rust中, 編譯器確保了永遠不會出現這個問題, 因為當你擁有引用時, 編譯器會確保資料不會在其引用之前離開作用域

fn main() {
    let res = dangle();  // 接收返回的引用
    println!("{}", res);
}

fn dangle() -> &String {
    let s = String::from("hello");  // s進入作用域

    &s  // 將s的引用返回
}  // 函式結束, s的資料會被清理, 但是s的引用返回出去了

這裡的 res 是函式 dangle 內部生成的變數的引用, 但是該函式內部的變數會結束後銷燬, 此時你獲取到的引用就是錯誤的, 就會發生懸垂引用的問題, Rust會在編譯時予以攔截

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership`.

如果有這樣的需求, 你應該直接返回變數, 而不是他的引用, 這樣會發生所有權的移動

fn main() {
    let res = dangle();  // 接收返回
    println!("{}", res);
}

fn dangle() -> String {
    let s = String::from("hello");  // s進入作用域

    s  // 將s返回
}  // 函式結束, s觸發了所有權的移動

slices

slice是沒有所有權的. slice允許你引用集合中某一短連續的元素序列, 而不引用整個集合

假設有這樣的需求, 寫一個函式, 接受一個字串, 返回字串中的第一個單詞. 如果函式在該字串中沒有找到空格, 那麼這整個就是一個單詞, 如果有空格, 則第一個空格前的是一個單詞

初版程式碼如下

fn first_word(s: &String) -> usize {  // 接收引用, 返回索引
    let bytes = s.as_bytes();  // 轉換成bytes元組
    for (i, &item) in bytes.iter().enumerate() {  // 生成迭代器並迴圈他
        // i是當前遍歷到的索引, &item是當前內容的引用
        if item == b' ' {  // 如果遇到了空格
            return i;  // 將索引return
        }
    }

    // 如果沒有找到, 證明全部都是一個單詞, 所以返回整體的索引
    s.len()
}

s是原本的字串的引用, 因為我們並不需要該字串的所有權

我們返回的是該字串中第一個單詞的索引

.as_bytes()是將字串轉換成bytes元組, .iter()是返回裡面的每一個值, 而.enumerate() 則是接收.iter()返回的值進一步包裝. 返回一個元組, 分為索引和值的引用, 噹噹前位元組為空格的時候, 證明需要返回了, 單詞結束, 於是將索引直接返回, 當遍歷完也沒有的時候證明整個字串都是一個單詞, 此時將整個長度返回

這樣看起來沒什麼問題, 但是這裡返回的索引長度其實與我們傳入的s不是繫結的, 我們在開發中可能遇到這樣的問題, 在某一個地方求出結果, 在後面呼叫時發現不匹配, 原來是源資料被改動了, 例如

fn main(){
    let mut s = String::from("word");
    let k = first_word(&s);
    s.clear();  // 這裡呼叫clear方法, 會獲取s的可變引用, 字串變成初始值, 也就是空串
    println!("{}", k)  // k依舊是原來的"word"時的結果
    // 後續中使用 k 就會出現問題, 因為s已經變更
}

因為 first_word 雖然需要了s的不可變引用, 但是返回值是普通的數字, 與s無關, 所以執行 first_word 之後不可變引用退出作用域了, 所以可以在 clear 裡順利的申請可變引用, 從而修改值

字串slice

字串slice是String中一部分值的引用

fn main(){
    let s = String::from("hello world");
    let h = &s[0..5];
    let w = &s[6..11];
    println!("h={},w={}", h, w)
}

如上面的程式碼. &s[0..5]代表引用了s的索引0-5之間的內容, 語法是[start_index..end_index], start_index是slice中的開始索引, end_index是slice中最後一個位置的後一個值索引. 例如[0..5]實際上是s的索引0到索引4, 也就是字串hello的引用, 我們執行檢視結果

h=hello,w=world

這種方法不是引用整個字串, 而是字串中的某一段

Rust的..range語法, 還有多種簡略寫法

如果從索引0開始, 可以忽略0, 可以達到一樣的效果

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

    let slice = &s[0..2];  // he
    let slice = &s[..2];  // he

如果一直到索引最後, 也可以捨棄尾部的數字

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

let len = s.len();

let slice = &s[3..len];  // llo
let slice = &s[3..];  // llo

如果是同時捨棄開頭和結尾, 則是將整個字串獲取

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

let len = s.len();

let slice = &s[0..len];  // hello
let slice = &s[..];  // hello

我們將之前的程式碼修改為新的字串slice引用的方式, 之後會解釋為什麼這樣做

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];  // 返回s的引用, 從0到當前空格的索引
        }
    }
    
    &s[..]  // 全部都是一個單詞, 就把整個返回
}

那麼這樣寫的好處是什麼呢? 回憶一下借用的規則, 當某個值已經有不可變引用時, 無法生成可變引用了, 對於s來講, 函式 fiest_word 返回的是s的不可變引用, 而後我們在嘗試改變s的值的時候.clear()嘗試申請s的可變引用, 這樣就會導致編譯時出現問題, 避免出現BUG, 我們按照之前的呼叫, 嘗試 .clear()

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:17:5
   |
15 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
16 | 
17 |     s.clear();
   |     ^^^^^^^^^ mutable borrow occurs here
18 | 
19 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.

會在編譯器就報錯, 防止出現BUG

字串字面值其實就是slice

原來在Rust中, 直接給變數賦值字串

let s = "s";

這裡, s型別就是 &str, 他是一個指向程式特定內部位置的slice, 所以他是不可變的, 因為就是不可變引用

字串slice作為引數

修改後的獲取單詞函式定義是

fn first_word(s: &String) -> &str {}

而更好的方式是定義為

fn first_word(s: &str) -> &str {}

這樣的目的是提高相容性, 上面說了, 使用let s = "s"; 型別是 &str, 所以新寫法可以相容這種字串, 當然對於String型別, 我們可以通過轉化成slice來使用

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

    // first_word 中傳入 `String` 的 slice
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // 因為字串字面值就是字串 slice,
    // 這樣寫也可以,即不使用 slice 語法!
    let word = first_word(my_string_literal);
  
  
    // &str 也可以繼續的生成 slice
    let word = first_word(&my_string_literal[..]);
}

其他型別的slice

字串slice裡面存放的是字串, 其實其他型別也是可以的

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

那麼這個slice的型別就是 &[i31], 使用方法與字串slice並無區別, 你可以對索引集合使用slice, 具體的資訊會在之後詳解

總結

所有權到這裡就結束了, 所有權, 借用和slice可以讓Rust程式變得更加的安全, 當你耐心的看到這裡, 可能你對Rust的獨特的程式設計思想有了大致的理解

Rust設定了諸多限制, 並且希望你寫出故意設卡(qia)的程式碼, 目的是讓程式更加安全, 在編譯期就把可能出現的問題暴露出來, 讓你去主動解決, 而不是在執行時, 或者是生產環境中才出現問題. 這需要開發者時刻留意遵循Rust的規範, 但是這一切都是值得的.

而Rust的所有權系統, 讓你無需關注垃圾的回收, 當然搭配作用域/引用/借用一起使用需要開發者關注變數的使用和作用域