Rust 筆記 - 基礎(chǔ)編程概念、Ownership

The Rust Programming Language

Rust 編程語(yǔ)言筆記。

來(lái)源:The Rust Programming Language By Steve Klabnik, Carol Nichols

翻譯參考:Rust 語(yǔ)言術(shù)語(yǔ)中英文對(duì)照表


Rustaceans

Rustaceans 指的是使用過(guò)、貢獻(xiàn)過(guò)或?qū)?Rust 語(yǔ)言有興趣的用戶。


安裝

使用 rustup 來(lái)安裝 Rust。

Rust 源文件以 .rs 作為擴(kuò)展名。

幾個(gè)常用的命令:

  • 編譯:rustc .rs-file
  • 運(yùn)行:./compiled-file
  • 檢查 Rust 編譯器版本:rustc --version
  • 檢查 rustup 版本:rustup --version
  • 更新 Rust:rustup update
  • 卸載 Rust 和 rustuprustup self uninstall
  • 本地文檔:rustup doc

Rust 是預(yù)(ahead-of-time)編譯語(yǔ)言,意思是可以把編譯后生成的可執(zhí)行文件發(fā)送給別人,他們可以在不安裝 Rust 的前提下運(yùn)行該文件。


Cargo

Cargo 是 Rust 的構(gòu)建系統(tǒng)和包管理工具。

幾個(gè)常用的命令:

  • 檢查 Cargo 的版本:cargo --version
  • 新建項(xiàng)目:cargo new
  • 項(xiàng)目構(gòu)建:cargo build
  • 運(yùn)行項(xiàng)目:cargo run
  • 項(xiàng)目檢查:cargo check:該命令確保項(xiàng)目可以編譯,但不生成可執(zhí)行文件,速度比 c--argo build 更快
  • 發(fā)布:cargo build --release

在運(yùn)行 cargo build 后,Rust 會(huì)把編譯后的二進(jìn)制文件放在 target/debug 文件夾下。

在 Rust 中,包被稱為 crates(也可以不翻譯)。

Rust 使用 TOML(Tom's Obvious Minimal Language) 格式來(lái)管理依賴。

使用 cargo new 創(chuàng)建新項(xiàng)目后,在該項(xiàng)目的目錄下會(huì)有名為 Cargo.toml 的文件來(lái)管理依賴和包。

可以使用 cargo add 來(lái)添加依賴,也可以在 .toml 文件的 [dependencies] 字段下添加包名。

在第一次使用 cargo build 后,Rust 會(huì)在根目錄生成名為 Cargo.lock 的文件用于追蹤包/依賴的版本。

例如:

[dependencies]
rand = "0.8.5"

包的版本使用三位的標(biāo)準(zhǔn)包標(biāo)準(zhǔn)。

.rs 文件中使用關(guān)鍵字 use 來(lái)導(dǎo)入包。


規(guī)范

  • Rust 的縮進(jìn)采用的是 4 個(gè) space,而不是 tab
  • Rust 使用 snake case 對(duì)變量和函數(shù)命名,也就是全部使用小寫字母,單詞中間使用下劃線 _ 連接,例如:var_name
  • Rust 使用 全大寫 對(duì)常量命名,單詞之間使用下劃線 _ 連接,例如:MINUTES_WITHIN_A_DAY

Hello World

fn main() {
    println!("Hello, World!");
}
  • fn 是函數(shù)(function)的關(guān)鍵詞
  • main() 是 Rust 的主函數(shù)、類似于 C、C++
  • 每行結(jié)束需要用分號(hào) ; 表示

基礎(chǔ)編程概念

注釋

Rust 有三種注釋:

  • 單行注釋: //
  • 多行注釋:/* */
  • 文檔(DocString)注釋://///!

變量和可變

變量

Rust 是靜態(tài)類型語(yǔ)言,在聲明變量時(shí)需要使用關(guān)鍵詞 let 并在冒號(hào) : 后指明變量的類型。這點(diǎn)類似于 Python 的 Type-Hint 以及 TypeScript。

let var: u8 = 0;

可變

Rust 中的所有變量都有可變(mutable)或不可變(immutable)。

如果在聲明變量時(shí)不明確指明,那么默認(rèn)為不可變。

使用關(guān)鍵字 mut 使得變量可變。

let mut var: u8 = 2;
u8 = 3; // That's OK

let ano_var: u8 = 2;
u8 = 3; // Error

隱藏、覆蓋 Shadow

fn main() {
    let x: u32 = 5;
    let x = x + 1;
      {
        let x = x * 2;
        println!("The inner x: {x}");
    }
      println!("The outer x: {x}");
}

// The inner x: 12
// The outer x: 6

如上述代碼所示:聲明了多個(gè)名為 x 的變量,在 Rust 中,我們稱 第一個(gè) x 被第二個(gè) x shadow(隱藏、覆蓋),或者 第二個(gè) x overshadow 了第一個(gè) x,這意味著:當(dāng)使用 x 這個(gè)變量時(shí),編譯器總是使用第二個(gè) x,即第二個(gè) x 拿走了第一個(gè) x 的所有,直到程序流離開第二個(gè) x 的作用域或者第二個(gè) x 也被 shadow。

shadow 和 mut 的區(qū)別

  1. shadow 使得可以改變同名變量的類型,但是 mut 會(huì)在編譯時(shí)出錯(cuò)

    fn main() {
        let x: &str = "     ";
       let x: usize = x.len(); // 5
      
        let mut x: &str = "     ";
        x = x.len()             // Error
    }
    
  2. shadow 使得可以變不可變的同名變量的值

    fn main() {
        let x: u32 = 5;
        x = 6;             // Error, 因?yàn)闆](méi)有變量默認(rèn)是不可變的
        
        let x: u32 = 5;
        let x = 6;
         println!("{}", x)  // 6
    }
    

常量

Rust 使用 const 關(guān)鍵字聲明常量。通常來(lái)說(shuō),需要用大寫字母聲明常量。

const THE_DAY_I_START_LEARNING_RUST: String = "2023.5.9";

不可變的變量和常量的區(qū)別在于:

  1. 常量只能是不可變的,不能通過(guò) mut 關(guān)鍵字使得常量可變
  2. 常量通常只賦值給常量聲明的表達(dá)式,而不是運(yùn)行時(shí)的計(jì)算表達(dá)式
  3. 常量可以在任何作用域(scope) 中聲明,在程序運(yùn)行期間,在其聲明的作用域中是有效的

數(shù)據(jù)類型

Rust 是靜態(tài)類型語(yǔ)言,因此在編譯時(shí)就需要知道所有的變量類型。

例如在數(shù)據(jù)轉(zhuǎn)換中:

fn main() {
    let var = "100".parse().expect("Not a number!"); // Error
    let var: u32 = "100".parse().expect("Not a number"); // OK, var == 100
}

基本類型

Rust 的基本數(shù)據(jù)類型有 4 種:

  • 布爾型 bool
  • 整型 integer
  • 浮點(diǎn)型 float
  • 字符型 char

布爾型

布爾型有兩個(gè)值:

  1. true
  2. false

整型

整型有 6 種長(zhǎng)度,每種長(zhǎng)度分為有符號(hào)無(wú)符號(hào)兩類。

長(zhǎng)度 有符號(hào) 無(wú)符號(hào)
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

整型字面量(literal)

字面量 舉例
十進(jìn)制 98_222
十六進(jìn)制 0xff
八進(jìn)制 0o77
二進(jìn)制 0b1111_0000
字節(jié)(u8) b'A'
  • 整型的默認(rèn)類型為 i32
  • 在使用集合類數(shù)據(jù)類型的索引時(shí),使用 usizeisize

浮點(diǎn)型

浮點(diǎn)型有兩類,而且都是有符號(hào)的:

  • f32
  • f64:默認(rèn)類型

字符型

字符型用 c (character)標(biāo)識(shí)。

復(fù)合類型

Rust 的復(fù)合類型包括:

  • 元組
  • 數(shù)組

元組

元組 tuple:用括號(hào) () 聲明,元組元素的數(shù)據(jù)類型可以不同。

元組是定長(zhǎng)的,一旦聲明后就不能改變大小。

let tup: (u32, f32, bool) = (1, 2.2, true);

Python 不同,Rust 中的元組可以使用 . 運(yùn)算符來(lái)訪問(wèn)。

let tup: (u32, f32, bool) = (1, 2.2, true);
println!("{}", tup.0); // 1
println!("{}", tup.2); // 2.2
println!("{}", tup.3); // true

沒(méi)有元素的元組被稱為單元(Unit),寫作 (),表示空值和空返回類型。

數(shù)組

數(shù)組 array: 用方括號(hào) [] 聲明,數(shù)組元素的數(shù)據(jù)類型必須相同

數(shù)組也是定長(zhǎng)的。

let arr: [u8; 5] = [1, 2, 3, 4, 5];
let arr_2: [3; 5]; // [5, 5, 5]

; 的前后分別表示數(shù)組元素的類型和數(shù)組元素的個(gè)數(shù)(如果前面用數(shù)字表示,則重復(fù)后面的數(shù)字前面次)。

可以使用索引 [],訪問(wèn)數(shù)組元素,因?yàn)閿?shù)組以定長(zhǎng)形式存儲(chǔ)在棧中:

let first = arr[0];

如果使用越界索引訪問(wèn)數(shù)組:

let arr: [u8; 5] = [1, 2, 3, 4, 5];
println!("{}", arr[5]); // runtime error

會(huì)導(dǎo)致 運(yùn)行時(shí) 錯(cuò)誤。Rust 會(huì)在運(yùn)行時(shí)檢查是否有越界行為。


控制流

條件

if, else if, else 等關(guān)鍵字控制程序流。

Rust 的條件語(yǔ)句不需要外嵌括號(hào) (),但是也不同于 Python 的 :,條件執(zhí)行體需要用花括號(hào):{}。

fn main() {
  let var: u32 = 8;
  if var > 0 {
    println!("{var} is greater than 0!");
  } else if var < 0 {
    println!("{var} is less than 0!");
  } else {
    println!("{var} is equal to 0!");
  }
}

注:與 JavaScript、Ruby 等語(yǔ)言不同,Rust 不會(huì)在條件判斷時(shí)自動(dòng)轉(zhuǎn)換變量的類型,例如:

fn main() {
    let var: bool = true;
  if var == 0 {
    println!("You can't get there in Rust");
  }
}

更簡(jiǎn)潔的寫法是在 let 中使用 if

fn main() {
  let condition: bool = true;
  let number: u32 = if condition { 5 } else { 6 };
  println!("{}", number) // 5
}

循環(huán)

Rust 中有 3 種循環(huán):

  • 無(wú)限循環(huán) loop
  • while 循環(huán)
  • for 循環(huán)

結(jié)束、跳出循環(huán)的關(guān)鍵字:

  • break 跳出當(dāng)前循環(huán)
  • continue 結(jié)束本次循環(huán)(開始下一次循環(huán))

loop 循環(huán)

Rust 可以使用關(guān)鍵字 loop 來(lái)創(chuàng)建無(wú)限循環(huán),例如:

loop {
  println!("This is a infinite loop");
}

// 等價(jià)于
while true {
  println!("This is a infinite loop");
}

continue 對(duì) loop 循環(huán)無(wú)效。

可以在 break 后添加跳出循環(huán)的返回值。

fn main() {
  let mut count = 0;
  let reture_value = loop {
    println!("{}", count);
    count += 1;
    if count == 5 {
        break count * 2;
    }
  }
}

可以用 ``nameloop` 循環(huán)命名(標(biāo)識(shí)符)以跳出多層循環(huán),例如:

fn main() {
  let mut count: u32 = 10;
    `outer: loop {
    let mut remaining: u32 = 2;
    loop {
      println!("{}", count * remaining);
      remaining -= 1;
      if remaining == 0 {
        break outer;
      }
    }
    count -= 1;
    println!("{}", count);
    if count == 0 {
      break;
    }
  }
}

while 循環(huán)

類似于其他語(yǔ)言的 while 循環(huán)。

同樣,Rust 的循環(huán)條件語(yǔ)句不需要外嵌圓括號(hào) (),但是也不同于 Python 的 :,循環(huán)體需要用花括號(hào):{}

fn main() {
  let mut count = 10;
  while count > 0 {
    println!("{}", count);
    count -= 1;
  }
}

for 循環(huán)

for in

for in 循環(huán)對(duì)遍歷對(duì)象中的每一個(gè)元素都執(zhí)行遍歷:

fn main() {
  let arr: [u32; 5] = [1, 2, 3, 4, 5];
  for element in arr {
    println!("{}", element);
  }
}

類似 Python 的同名函數(shù)。

fn main() {
  for i in 0..5 {
    println!("{}", i);
  }
}

使用 .rev() 倒序輸出:

fn main() {
  for i in (0..5).rev() {
    println!("{}", i);
  }
}

函數(shù)

使用關(guān)鍵字 fn 聲明函數(shù),Rust 總是首先執(zhí)行 main 函數(shù)。

fn main() {
  let var: u32 = 3;
  increment_and_print(var);
}

fn increment_print(num: u32) -> u32 {
    num += 1;
  println!("{}", num);
  return num;
  // num
}

如上述代碼所示:Rust 并不關(guān)心函數(shù)定義的位置,因此在 main 函數(shù)前后定義其他函數(shù)都不會(huì)錯(cuò)誤。

函數(shù)的參數(shù)需要聲明名稱和類型,返回值用 -> 表示,只需要聲明類型。

可以使用關(guān)鍵字 return 顯式返回值,也可以把返回值寫在最后一行(不要使用 ; 結(jié)尾)。Rust 默認(rèn)把最后一行的值作為返回值。

語(yǔ)句 VS 表達(dá)式

Rust 是基于表達(dá)式的語(yǔ)言。在 Rust 中,語(yǔ)句(statements)和表達(dá)式(expressions)有明顯的區(qū)別:

  • 語(yǔ)句:一段完成行為的指令,返回值,以分號(hào)結(jié)尾。
  • 表達(dá)式:給結(jié)果返回值,可以是語(yǔ)句的一部分,以分號(hào)結(jié)尾(通過(guò)添加分號(hào)把表達(dá)式轉(zhuǎn)化為語(yǔ)句,同時(shí)失去返回值)
let y  = (let x = 6); // Error

上面的代碼中,賦值語(yǔ)句并不返回任何值,因此,不能把 let x = 6 賦值給 y。這不同于 C、Ruby 等語(yǔ)言,其賦值語(yǔ)句會(huì)返回賦予的值。

而對(duì)于表達(dá)式:

{
  let x: u32 = 6;
  x + 1 // 返回 7
}

輸入和輸出

輸入

使用標(biāo)準(zhǔn)庫(kù) std::io 來(lái)導(dǎo)入標(biāo)準(zhǔn)庫(kù)的 io。

use std::io;

fn main() {
  let mut inp: String = String::new();
    io::stdin.read_line(&mut inp).expect("Failed to read"); 
  println!("{inp}");
}
  • 使用 stdin 引入標(biāo)準(zhǔn)輸入流,
  • 使用 read_line(&) 來(lái)讀取輸入,
  • 使用 expect() 來(lái)處理異常情況。

輸出

使用 println!() 來(lái)輸出內(nèi)容。

println!() 表示調(diào)用 Rust 宏(macro),println() 表示調(diào)用函數(shù)

Rust 不支持直接輸出變量的值,必須格式化(format)后輸出。在引號(hào) "" 中使花括號(hào) {} 來(lái)輸出變量。

fn main() {
    let var: u32 = 15;
    println!("{}", var); // 15
    // println!("var equals {var}")  Also ok!
    // println!(var)  Error!
}

所有權(quán) Ownership

Ownership 是 Rust 語(yǔ)言管理內(nèi)存的規(guī)則。

Ownership 是 Rust 語(yǔ)言最重要的特性之一,也是為什么 Rust 比 C++ 更安全的原因。

Ownership

棧內(nèi)存和堆內(nèi)存

要理解什么是 Ownership,首先要了解棧內(nèi)存(stack)和堆內(nèi)存(heap)的區(qū)別。

簡(jiǎn)單來(lái)講,棧內(nèi)存和堆內(nèi)存都是程序在運(yùn)行時(shí)使用的內(nèi)存。

棧最重要的操作是入棧和出棧,入棧時(shí)把元素放在棧頂,出棧時(shí)把棧頂元素彈出,因此棧內(nèi)存中的元素是先后順序的。棧內(nèi)存中存儲(chǔ)的是定長(zhǎng)的數(shù)據(jù),而在編譯時(shí)未知大小或者大小可能改動(dòng)的數(shù)據(jù)會(huì)存儲(chǔ)在堆內(nèi)存中。

堆內(nèi)存中的元素沒(méi)有先后順序,每次都要給元素分配足夠大的空間。當(dāng)數(shù)據(jù)需要存放在堆中時(shí),內(nèi)存分配器會(huì)分配給數(shù)據(jù)一塊空閑區(qū)域,并返回一個(gè)指針,指向這塊區(qū)域的地址。該指針是定長(zhǎng)的,因此可以存儲(chǔ)在棧中,但是如果要訪問(wèn)堆中的數(shù)據(jù),必須要沿著指針指向/存儲(chǔ)的地址。

因此,內(nèi)存存儲(chǔ)數(shù)據(jù)和訪問(wèn)數(shù)據(jù)時(shí)速度都要更,而堆內(nèi)存則正好相反,因?yàn)槠浞謩e需要尋找空閑的內(nèi)存空間以及沿著指針尋找地址。

垃圾回收和動(dòng)態(tài)分配

對(duì)于靜態(tài)類型語(yǔ)言來(lái)說(shuō),所有變量都在內(nèi)存中占有一定的空間,該空間位置在 C、C++ 等語(yǔ)言中使用指針來(lái)表示。對(duì)于不使用的變量所占有的空間的釋放是編程語(yǔ)言設(shè)計(jì)時(shí)的關(guān)鍵問(wèn)題。通常應(yīng)該及時(shí)釋放其占有的空間,否則可能導(dǎo)致堆內(nèi)存溢出,引起難以預(yù)料的安全問(wèn)題;但如果釋放太早,則可能導(dǎo)致后續(xù)對(duì)該變量的引用出錯(cuò)。

對(duì)于該問(wèn)題,有一些語(yǔ)言(例如:Java)使用一種被稱為“垃圾回收”(Garbage Collection / GC)的機(jī)制來(lái)追蹤并釋放不使用變量的內(nèi)存。另一些語(yǔ)言(例如:C、C++)則需要用配對(duì)的 malloc(), free() 等函數(shù)來(lái)動(dòng)態(tài)分配和釋放空間。

Rust 則采用了 Ownership 機(jī)制。

Ownership 規(guī)則

  • Rust 中的每個(gè)值有一個(gè) owner
  • 在同一時(shí)刻只能有一個(gè) owner
  • 當(dāng) owner 超出作用域時(shí),該值會(huì)被丟棄

變量 ownership 的轉(zhuǎn)換

來(lái)看一個(gè)例子:如果兩個(gè)變量指向同一段內(nèi)存空間,此時(shí)如果釋放第一個(gè)變量指向的空間(也是第二個(gè)變量的指向),那么引用第二個(gè)變量是否會(huì)導(dǎo)致錯(cuò)誤?

fn main() {
  let string_in_stack: &str = "This string is stored in stack";
  let ano_string_in_stack: &str = string_in_stack;
  println!("{}", string_in_stack);
  println!("{}", ano_string_in_stack);
  
  let string_in_heap: String = String::from("This string is stored in heap");
  let ano_string_in_heap: String = string_in_heap;
  println!("{}", string_in_heap); // value borrowed here after move
  println!("{}", ano_string_in_heap);
}

上面的例子分別聲明了兩類字符串變量:

  • string_in_stack 存儲(chǔ)在內(nèi)存中。也稱為“字符串字面量”,指的是通過(guò)雙引號(hào)來(lái)表示的字符串,這類字符串是不可變的(這里的不可變指的是不能更改字符串中某個(gè)字符,因?yàn)槠浯鎯?chǔ)是連續(xù)的),但可以變整個(gè)字符串。這類字符串更加高效,原因是在編譯時(shí)就已經(jīng)知道了其大小,而且是定長(zhǎng)的。

  • string_in_heap 存儲(chǔ)在內(nèi)存中。是由 String 類型創(chuàng)建的字符串,可以變。由于這類字符串是變長(zhǎng)的,因此在編譯時(shí)是不知道其內(nèi)容的,只能在運(yùn)行時(shí)請(qǐng)求在堆中分配內(nèi)存(通過(guò) String::from 完成)。

    當(dāng)使用完該類字符串后,需要一種機(jī)制把內(nèi)存還給分配器:

    • 帶有 GC 的語(yǔ)言會(huì)追蹤并清理不使用的內(nèi)存空間
    • 沒(méi)有 GC 的語(yǔ)言需要程序員去識(shí)別并釋放內(nèi)存
    • Rust 的做法是:一旦變量超出其作用域,該變量占據(jù)的內(nèi)存將被自動(dòng)回收(調(diào)用 drop 函數(shù))

深、淺拷貝,移動(dòng)和克隆

回到代碼,存儲(chǔ)在上的字符串 string_in_heap 的內(nèi)存會(huì)被收回。這有些類似于淺拷貝(shallow copy),其只復(fù)制指針,長(zhǎng)度和 capacity,但是并不復(fù)制原數(shù)據(jù);但是 Rust 同時(shí)還釋放了被拷貝變量的空間,所以該操作被稱為移動(dòng)(move)

因此,Rust 從來(lái)不會(huì)自動(dòng)執(zhí)行深拷貝(deep copy)

如果確實(shí)要“深拷貝”堆中的數(shù)據(jù),可以使用 .clone()。

fn main() {
    let string_in_heap: String = String::from("Hello");
    let ano_string_in_heap: String = string_in_heap.clone();
    println!("{} {}", string_in_heap, ano_string_in_heap);
}

// Hello Hello

因此,變量 ownership 的變化模式是相同的:

  • 每當(dāng)賦值給另一變量時(shí),ownership move
  • 當(dāng)堆中的變量超出作用域時(shí),ownership drop,除非 ownership 又被移交給其他的變量

Copy、Drop

Rust 中有一個(gè)被稱為 Copy 的 特質(zhì)(trait),可用于某種數(shù)據(jù)類型。實(shí)現(xiàn)了 Copy 的數(shù)據(jù)類型的變量可以賦值給另一個(gè)變量,而不會(huì)導(dǎo)致 move 操作。

Rust 不允許在實(shí)現(xiàn)了 Drop 特質(zhì)(trait) 的數(shù)據(jù)類型上實(shí)現(xiàn) Copy

實(shí)現(xiàn) Copy 特質(zhì)(trait) 的數(shù)據(jù)類型包括:

  • 所有的整型
  • 所有的布爾型
  • 所有的浮點(diǎn)型
  • 所有的字符型
  • 只包含上述四種的類型作為成員的元組

函數(shù)和返回值的 Ownership

函數(shù)和變量類似,給函數(shù)傳遞值會(huì)導(dǎo)致 move 或者 copy。

返回值也會(huì)轉(zhuǎn)移 ownership。

如果要讓函數(shù)使用變量但是不轉(zhuǎn)移 ownership,可以使用引用(reference)。

引用

引用和借用

引用(reference)類似于指針,它指向存儲(chǔ)某個(gè)數(shù)據(jù)的內(nèi)存。與指針不同的是,引用指向的數(shù)據(jù)總是有效的。

引用使得我們不需要帶走 ownership 就可以獲取數(shù)據(jù)。

使用引用的行為稱為借用(borrowing)。

使用引用

使用 & 來(lái)創(chuàng)建一個(gè)引用。

在使用引用類型時(shí),需要修改函數(shù)簽名,傳參方式等,例如:

fn main() {
        let s1 = String::from("Hello");
    let length = calculate_length(&s1); // 傳入引用 &
    // The length of s1: 5
    println!("The length of s1: {}", length); 
}

fn calculate_length(s: &String) -> usize {  // 函數(shù)參數(shù)類型為引用 &
        s.len()
}

引用原則

在 Rust 中,引用的原則是:

  1. 只能同時(shí)一個(gè)可變引用或者多個(gè)不可變引用
  2. 引用必須有效(valid)

可變和不可變引用

上述的引用是不可變的。類似于變量,如果想改變變量的值,需要使用可變引用。

& 后添加 mut 關(guān)鍵字來(lái)使用可變引用。

fn main() {
        let mut s1 = String::from("Hello");
    change(&mut s1); // 添加 mut
    // s1: "Hello, World"
    println!("s1: {}", s1); 
}

fn change(s: &mut String) {  // 添加 mut
        s.push_str(", World")
}

可以同時(shí)使用多個(gè)不可變引用:

fn main() {
        let mut s1 = String::from("Hello");
    let r1 = &s1; // Ok
    let r2 = &s1; // Ok
}

但是只能同時(shí)使用一個(gè)可變引用:

fn main() {
        let mut s1 = String::from("Hello");
    let r1 = &mut s1; // Ok (first borrow)
    let r2 = &mut s1; // Err (cannot borrow)
}

也不能同時(shí)使用可變和不可變引用:

fn main() {
        let mut s1 = String::from("Hello");
    let r1 = &s1; // Ok (first borrow)
    let r2 = &mut s1; // Err (cannot borrow)
}

除非作用域不同:

fn main() {
        let mut s1 = String::from("Hello");
    {
            let r1 = &mut s1; // Ok (first borrow) 
    }
    let r2 = &mut s1; // Ok
}

引用的作用域

引用的作用域從聲明開始到最后一次使用該引用。

例如下面的代碼沒(méi)有問(wèn)題

fn main() {
        let mut s1 = String::from("Hello");
    let r1 = &s1; // Ok (first borrow) 
      let r2 = &s1; // Ok (second borrow) 
    println!("r1 and r2 {} {}", r1, r2); // r1 and r2 no longer used
  
    let r3 = &mut s1; // Ok!
}

數(shù)據(jù)競(jìng)爭(zhēng)

Rust 的引用原則避免了數(shù)據(jù)競(jìng)爭(zhēng)可能導(dǎo)致的編譯時(shí)錯(cuò)誤。

數(shù)據(jù)競(jìng)爭(zhēng)指的是下述三種行為:

  1. 兩個(gè)或以上只能同時(shí)指向同一個(gè)數(shù)據(jù)
  2. 至少一個(gè)指針被用于寫入數(shù)據(jù)
  3. 沒(méi)有同步機(jī)制讀取數(shù)據(jù)

數(shù)據(jù)競(jìng)爭(zhēng)可能導(dǎo)致未定義的行為而且在運(yùn)行時(shí)難以診斷和調(diào)試。Rust 的引用限制能夠有效避免這類問(wèn)題。

有效引用和懸垂指針

懸垂指針(dangling point) 是指向了已經(jīng)被 free 內(nèi)存區(qū)域的指針。

在 Rust 中,編譯器保證引用必須有效。也就是引用必須發(fā)生在數(shù)據(jù)超出作用域前。

fn main() {
        let mut s1 = return_string(); // error
}

fn return_string() -> &String {
        let s = String::from("Hello");
    &s  
} // s out of scope

/* correct the above code
fn return_string() -> String {
        let s = String::from("Hello");
    s   // return string not reference
}
*/

切片類型

切片(slice) 指的是對(duì)集合的部分引用。

語(yǔ)法

切片的語(yǔ)法為:[start_index..end_index],其中:

  • .. 是范圍(range)運(yùn)算符
  • start_index 是開始的索引。(如果忽略該項(xiàng),默認(rèn)從 0 開始)
  • end_index 是最后的索引(不包括該索引位置)。(如果忽略該項(xiàng),默認(rèn)以集合的末尾截止)

字符串切片

下面是字符串切片:

fn main() {
        let s1 = String::from("Hello, World");
    let slice1 = &s1[0..5];
    let slice2 = &s1[7..12];
    println!("{} {}", slice1, slice2);
}

// Hello World

字符串切片的類型為 &str,因?yàn)槭遣豢勺円?& (非 & mut),因此,字符串字面量是不可變的。

解引用強(qiáng)制轉(zhuǎn)化

如果已知字符串切片,我們可以直接把它作為參數(shù)傳入;如果已知 String 類型,我們可以傳入該類型的切片或者對(duì) String 的引用。這種靈活性稱為解引用強(qiáng)制轉(zhuǎn)化(deref coercion)

有經(jīng)驗(yàn)的 Rustaceans 會(huì)把 &String 類型為參數(shù)的函數(shù)簽名更改為 &str

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

這樣的改動(dòng)使得函數(shù)更加通用(general)同時(shí)并不損失任何功能。

例如:

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

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

如果 first_word 函數(shù)的參數(shù)類型為 &String:

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[..]
}

那么上述代碼就會(huì)報(bào)錯(cuò)!。

如果為 &str:

fn first_word(s: &str) -> &str { // 只修改函數(shù)參數(shù)的類型
        // --snip--
}

代碼就能成功執(zhí)行。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容