Rust 學習筆記:參考與切片

October 6, 2023 · Chinese


前言

本系列文書寫我個人初探 Rust 的學習筆記,章節劃分主要基於著名的 The BookThe Rust Programming Language,程式碼部分通常是個人閱讀消化後以方便說明的方式撰寫,完整學習建議直接參見該書。

The Rust Programming Language

該書也有中文翻譯版,不過個人閱讀以英文原版為主以鞏固對 terminology 的一致認識,我認為對未來閱讀以及查找資料會較為順暢。

不論語言該書都是相當優秀的學習資源,選擇適合你的語言開始學習 Rust 吧。

Appendix F: Translations of the Book

參考

A reference is like a pointer in that it’s an address we can follow to access the data stored at that address;

在 Rust 中參考就像是 pointer,和 pointer 的差異是在 Rust 中參考保證值是 valid 的。

參考以下程式,say_hello 接受一個 name 參數,print 出結果。這是我們以往的作法:

fn main() {
    let s = String::from("Josh");
    
    say_hello(s);
    
    println!("{}", s); // ERROR: borrow of moved value: `s`
}

fn say_hello(name: String){
    println!("Hello {}!", name);
}

這邊會發現若我們在後面要繼續使用 s 會出現報錯,因為 s 在傳入函式時產生了所有權的移轉。若我們要再次使用會需要在函式中把 s 再 return 出來,即把所有權轉移回來。

另一種方式是使用參考,寫法是以 & prefix:

fn main() {
    let s = String::from("Josh");
    
    say_hello(&s);  // Hello Josh!
    
    println!("{}", s);  // Josh
}

fn say_hello(name: &String){
    println!("Hello {}!", name);
}

利用參考不產生所有權移轉,因此我們即使不做回傳也能在 main 中繼續使用 s

可變參考

參考與值一樣預設是不可變的,一樣可以利用 &mut 讓其可變。

let mut s = String::from("Josh");
let s1 = &s;

s1.push('1');     // cannot borrow `*s1` as mutable, as it is behind a `&` reference

println!("{}", s1);
let mut s = String::from("Josh");
let s1 = &mut s;

s1.push('1');

println!("{}", s1);     // Josh1

參考的規則

  • 同時只能有無限的不可變參考,或一個可變參考。
  • 參考必須永遠合法 (valid)

不可變參考數量不限

let mut s = String::from("Josh");
let s1 = &s;
let s2 = &s;

println!("{}", s1);     // Josh
println!("{}", s2);     // Josh

可變參考只能有一個

let mut s = String::from("Josh");
let s1 = &mut s;
let s2 = &mut s;

println!("{}", s1);     // ERROR: cannot borrow `s` as mutable more than once at a time
println!("{}", s2);

不可同時有不可變參考與可變參考

let mut s = String::from("Josh");
let s1 = &s;
let s2 = &mut s;

println!("{}", s1);
println!("{}", s2);     // ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable

迷途指標 dangling pointer

Rust 中會保證參考的合法性,產生迷途指標會直接報錯。

以下是簡單的範例程式,在 get_dangling_pointer 我們 return s 的參考,然而我們知道 s 在函式結束後便被釋放,我們回傳的參考便指向一個已經被釋放的東西上,即為迷途指標。

Rust 在這種情況給出的錯誤訊息為 missing lifetime specifier,涉及到之後會學習的 lifetime。

fn main() {
    let s = get_dangling_pointer();     // missing lifetime specifier
}

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

切片 Slice

看了上述的迷途指標,顯然我們無法在函式內建立新的 String 將其回傳。

切片型別便是一種解決方案,字串切片是字串某一段部分的參考。

不只字串有切片型別,

let s = String::from("Hello Josh");
let hello = &s[0..5];
println!("{}", hello); // Hello

使用 [..] 語法指定一段範圍,若以字串開頭為起始則 0 可省略,以字串末尾為結束則最後的 index 可省略。

let s = String::from("Hello");
println!("{}", &s[1..4]);   // ell

println!("{}", &s[0..5]);   // Hello
println!("{}", &s[..5]);    // Hello
println!("{}", &s[0..]);    // Hello
println!("{}", &s[..]);    // Hello

String literal

string literal 的型別為字串切片 &str

let s: &str = "Hello";

切片作為參數

以字串切片作為參數會比 &String 更具彈性,同時能夠接受切片與字串參考。

fn main() {
    let name_string = String::from("Josh");
    let name_slice = &name_string[..];
    
    say_hello(&name_string);    // OK: Hello Josh
    say_hello(name_slice);      // OK: Hello Josh
}

fn say_hello(name: &str){
    println!("Hello {}", name);
}
fn main() {
    let name_string = String::from("Josh");
    let name_slice = &name_string[..];
    
    say_hello(&name_string);    // OK: Hello Josh
    say_hello(name_slice);      // ERROR: mismatched types
}

fn say_hello(name: &String){
    println!("Hello {}", name);
}

切片作為回傳值

有了切片我們有回傳一部分字串的方法了。

下面程式回傳字串的前半段。

fn main() {
    let s = "I am handsome";
    
    let first_half = get_first_half(s);
    
    println!("{}", first_half);     // I am h
}

fn get_first_half(s: &str)->&str{
    &s[..s.len() / 2]
}