Rust 基礎筆記

通用編程概念

變量與可變性

  1. 變量默認不可變,如需要改變,可在變量名前加 mut 使其可變。例如:let mut a=1;
  2. 常量總是不能改變,使用 const 聲明,并且必須注明值的類型。例如:const MAX_NUM: u32 = 100;
  3. 可以定義一個與之前變量同名的新變量,從而“遮蓋”之前的變量,如下:
    let x = 5;
    let x = 6;
    let x = x * 2;
    println!("The value of x is: {}", x); // 12

mut 與“遮蓋”的區別是,當再次使用 let 時,實際上創建了一個新變量,可以改變值的類型。而 mut 還是原來的變量,不可以改變類型。

數據類型

Rust是靜態類型語言,任何值都屬于一種明確的類型,內建類型分為:標量(scalar)復合(compound)

標量類型

標量 類型代表一個單獨的值。Rust 有四種基本的標量類型:整型、浮點型、布爾類型、和字符類型

  • 整型
    整數是一個沒有小數部分的數字,每一種變體都可以是有符號或無符號的,并有一個明確的大小。有符號和無符號代表數字能否為負值。
長度 有符號 無符號
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

isizeusize 類型依賴運行程序的計算機架構:64 位架構上它們是 64 位的, 32 位架構上它們是 32 位的。

數字字面值 示例
十進制 98_222
十六進制 0xff
八進制 0o77
二進制 0b111_000
字節(u8) b'a'

可以使用表格中的任何一種形式編寫數字字面值。除字節以外 的其它字面值允許使用 類型后綴,例如57u8,允許使用 _ 做為分隔符以方便讀數,例如 1_000 (分隔符的數量與位置并不影響實際的數字)。

Rust 默認數字類型是 i32:它通常是最快的,甚至在 64 位系統上也是。isizeusize 的主要作為集合的索引。

  • 浮點型

Rust 同樣有兩個主要的浮點數類型:f32(單精度浮點數)f64(雙精度浮點數),分別占 32 位和 64 位比特。默認類型是 f64。因為它與 f32 速度差不多,然而精度更高。在 32 位系統上也能夠使用 f64。不過比使用 f32 要慢。

  • 布爾型

Rust 中的布爾類型有兩個:truefalse。Rust 中的布爾類型使用 bool 表示。

  • 字符類型

Rust 的 char 類型代表了一個 Unicode 標量值。這意味著它可以比 ASCII 表示更多內容。拼音字母、中文/日文/漢語等象形文字、emoji(絵文字)以及零長度的空白字符對于 Rust char類型都是有效的。Unicode 標量值包含從 U+0000U+D7FFU+E000U+10FFFF 之間的值。“字符”并不是一個 Unicode 中的概念,Rust 中的 char類型與直覺上的“字符”可能并不符合。

復合類型

復合類型 可以將多個其它類型的值組合成一個類型。Rust 有兩個原生的復合類型:元組(tuple)數組(array)

  • 元組

元組是一個將多個其它類型的值組合進一個復合類型的主要方式。

元組中的每一個位置都有一個類型,類型不必相同。

為了從元組中獲取單個的值,可以使用模式匹配(pattern matching)來解構(destructure )元組。

    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;
    println!("The value of y is: {}", y); // 6.4

除了使用模式匹配解構之外,也可以使用點號(.)后跟值的索引來直接訪問它們,元組的第一個索引值是 0。例如:

    let x: (i32, f64, u8) = (500, 6.4, 1);
    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
  • 數組

數組中的每個元素的類型必須相同。

Rust 中的數組是 固定長度 的:一旦聲明,它們的長度不能增長或縮小。

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

數組在想要在 (stack)上分配空間,或者是想要確保總是有固定數量的元素時十分有用。雖然它并不如 vector 類型那么靈活。vector 類型是標準庫提供的一個允許增長和縮小長度的類似數組的集合類型。當不確定是應該使用數組還是 vector 的時候,你可能應該使用 vector

數組是一整塊分配在棧上的內存。可以使用索引來訪問數組的元素,像這樣:

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

當嘗試用索引訪問一個元素時,Rust 會檢查指定的索引是否小于數組的長度。如果索引超出了數組長度,Rust 會panic,它用于程序因為錯誤而退出的情況。

函數

Rust 使用 main 函數作為程序的入口。使用 fn 關鍵字類聲明函數。使用 “snake case” 作為函數和變量的命名規范——所有字母都小寫并使用下劃線分割。

在函數簽名中,必須聲明每個參數的類型。這是 Rust 設計中一個經過慎重考慮的決定:要求在函數定義中提供類型注解意味著編譯器再也不需要在別的地方要求你注明類型就能知道你的意圖。

語句與表達式

Rust 是一個基于表達式的語言。

  • 語句(Statements) 是執行一些操作但不返回值的指令。函數定義也是語句,語句并不返回值。不能把語句賦值給一個變量。

  • 表達式(Expressions) 計算并產生一個值。函數調用是一個表達式,宏調用是一個表達式,用來創新建作用域的大括號(代碼塊){} 也是一個表達式。表達式并不包含結尾的分號。如果在表達式的結尾加上分號,就變成了語句。

函數的返回值

可以向調用它的代碼返回值,并不對返回值命名,不過會在一個箭頭(->)后聲明它的類型。在 Rust 中,函數的返回值等同于函數體最后一個表達式的值。這是一個有返回值的函數的例子:

fn five() -> i32 {
    5
}

fn main() {
    let x = five();
    println!("The value of x is: {}", x);
}

控制流

Rust 代碼中最常見的用來控制執行流的結構是if表達式循環

  • if 表達式

所有if表達式以 if 關鍵字開頭,后跟一個條件,條件必須是 bool(不需要圓括號)。Rust 并 不會 自動將非布爾值轉換為布爾值。

let number = 6;

if number % 3 == 0 {
    println!("number is divisible by 3");
} else if number % 2 == 0 {
    println!("number is divisible by 2");
} else {
    println!("number is not divisible by 4, 3, or 2");
}

使用過多的 else if 表達式會使代碼顯得雜亂無章,所以如果有多于一個 else if ,最好使用 match 重構代碼。

由于if是表達式,所以可以用在let變量聲明中,但是必須注意值的類型一致,必須包含有 else 塊,并且別忘了結尾的分號。

  • 循環

rust 有三種循環類型:loopwhilefor

  1. 無限循環loop
loop {
    println!("again!");
}

可以使用 break 關鍵字來告訴程序何時停止執行循環。

  1. 條件循環 while
let mut number = 3;
while number != 0  {
    println!("{}!", number);
    number = number - 1;
}
println!("LIFTOFF!!!");
  1. 集合遍歷 for
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
    println!("the value is: {}", element);
}

for 循環的安全性和簡潔性使得它在成為 Rust 中使用最多的循環結構。即使是在想要循環執行代碼特定次數時,使用 Range,它是標準庫提供的用來生成從一個數字開始到另一個數字結束的所有數字序列的類型。例如:

for i in 1..10 {
  println!("value is:{}",i);
}

還可以使用 rev 方法來反轉 Range

for i in (1..10).rev() {
    println!("value is:{}",i);
}

所有權

堆與棧

  • 棧: 后進先出,增加數據叫進棧,移除數據叫出棧。

操作棧是非常快的,因為它訪問數據的方式:永遠也不需要尋找一個位置放入新數據或取出數據,因為這個位置總是在棧頂。所有數據都必須是一個已知的固定大小。

  • 堆:訪問堆上的數據要比訪問棧上的數據要慢,因為必須通過指針來訪問。

當調用一個函數,傳遞給函數的值(包括可能指向堆上數據的指針)和函數的局部變量被壓入棧中。當函數結束時,這些值被移除棧。

所有權規則

  1. 每一個值都被它的所有者(owner)變量擁有。
  2. 值在任意時刻只能被一個所有者擁有。
  3. 當所有者離開作用域,這個值將被丟棄。

String 類型

String 類型存儲在堆上,可以用 from 從字符串字面值來創建 String 如:let s = String::from("hello");

對于String類型,為了支持一個可變,可增長的文本片段,需要在堆上分配一塊在編譯時未知大小的內存來存放內容。這意味著:

  1. 內存必須在運行時向操作系統請求。

  2. 需要一個當我們處理完String 時將內存返回給操作系統的方法。

Rust 采取了一個不同的策略:內存在擁有它的變量離開作用域后就被自動釋放。當變量離開作用域,Rust 為其調用一個特殊的函數。這個函數叫做 drop。在這里String的作者可以放置釋放內存的代碼。Rust 在結尾的}處自動調用 drop

String 由三部分組成,如下圖左側所示:一個指向存放字符串內容內存的指針,一個長度,和一個容量。這一組數據儲存在棧上。右側則是堆上存放內容的內存部分。

堆 棧 內存分配
堆 棧 內存分配

長度代表當前String的內容使用了多少字節的內存。容量是String從操作系統總共獲取了多少字節的內存。

拷貝指針、長度和容量而不拷貝數據可能聽起來像淺拷貝。在Rust中這個操作被稱為移動(move),而不是淺拷貝,移動是指所有權的轉移。Rust 永遠也不會自動創建數據的“深拷貝”。當 move 發生的時候,所有權被轉移的變量,將會被釋放。

如果我們確實需要深度復制String中堆上的數據,而不僅僅是棧上的數據,可以使用一個叫做clone()的通用函數。

任何簡單標量值的組合可以是Copy的,對于實現了Copy Trait的類型來說,當移動發生的時候,它們可以Copy的副本代替自己去移動,而自身還保留著所有權。

  • 所有整數類型,比如u32
  • 布爾類型,bool,它的值是truefalse
  • 所有浮點數類型,比如f64
  • 元組,當且僅當其包含的類型也都是Copy的時候。如:(i32, i32)是Copy的,不過(i32, String)就不是。

值得注意的是,Rust 不允許自身或其任何部分實現了Drop trait 的類型使用Copy trait。

將值傳遞給函數在語言上與給變量賦值相似。向函數傳遞值可能會移動或者復制,就像賦值語句一樣。所以如 String 類型,如果將它作為參數傳遞給函數后,就會產生移動,之后不能在使用。

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

    takes_ownership(s); // 傳遞參數和變量賦值相似 s產生了移動,所有權被轉移
    println!("{}", s); // error: value used here after move
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

變量的所有權總是遵循相同的模式:將值賦值給另一個變量時移動它。當持有堆中數據值的變量離開作用域時,其值將通過drop被清理掉,除非數據被移動為另一個變量所有。

作用域

作用域在Rust中的作用就是制造一個邊界,這個邊界是所有權的邊界。變量走出其所在作用域,所有權會move。如果不想讓所有權 move ,則可以使用 “引用” 來“出借”變量,而此時作用域的作用就是保證被“借用”的變量準確歸還。

在當前作用域中由 let 開啟的作用域是隱式作用域。在 Rust 中,也有一些特殊的宏,比如 println!(),也會產生一個默認的作用域,并且會隱式借用變量。

引用和借用

使用&符號,就是引用,允許使用其值但不獲取它的所有權。將獲取引用作為函數參數稱為借用

正如變量默認是不可變的,引用也一樣。不允許修改引用的值。

可變引用

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

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

可變引用有一個很大的限制:在特定作用域中一個數據有且只有一個可變引用,也不能在擁有不可變引用的同時擁有可變引用。這個限制的好處是 Rust 可以在編譯時就避免數據競爭(data races)。

數據競爭是一種特定類型的競爭狀態,它可由這三個行為造成:

  1. 兩個或更多指針同時訪問同一數據。
  2. 至少有一個指針被寫入。
  3. 沒有同步數據訪問的機制。

引用的規則

  1. 在任意給定時間,只能擁有如下中的一個:
    • 一個可變引用。
    • 任意數量的不可變引用。
  2. 引用必須總是有效的。

Slices

slice數據類型,沒有所有權。它允許引用集合中一段連續的元素序列,而不用引用整個集合。

字符串 slice

字符串 slice(string slice)是 String 中一部分值的引用,它看起來像這樣:

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

let hello = &s[0..5]; // 也可以寫成 &s[..5]
let world = &s[6..11]; // &s[6..]
let hw = &s[..] // 截取整個字符串

不同于整個String的引用,這是一個包含String內部的一個位置和所需元素數量的引用。

字符串字面值就是 slice 比如:let s = "Hello, world!"; 這里 s 的類型就是 &str:它是一個指向二進制程序特定位置的slice。這也就是為什么字符串字面值是不可變的;&str 是一個不可變引用

可以對其它所有類型的集合使用 slice 例如:let a = [1, 2, 3, 4, 5];let slice = &a[1..3]; 這個 slice 的類型是 &[i32]

結構體

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

為了從結構體中獲取某個值,可以使用點號 . 。如果我們只想要用戶的郵箱地址,可以用 user1.email

User 結構體的定義中,我們使用了自身擁有所有權的String類型而不是 &str 字符串 slice 類型。因為我們想要這個結構體擁有它所有的數據,為此只要整個結構體是有效的話其數據也應該是有效的。

聲明結構體,字段必須要聲明類型,否則會報錯。

可以使結構體儲存被其它對象擁有的數據的引用,不過這么做的話需要用上 生命周期(lifetimes)。生命周期確保結構體引用的數據有效性跟結構體本身保持一致。

方法

方法與函數類似:使用fn關鍵字和名字聲明,可以擁有參數和返回值,同時包含一些代碼會在某處被調用時執行。

與函數不同的是,它們在結構體(或者枚舉或者 trait 對象)的上下文中被定義,并且它們第一個參數總是 self,它代表方法被調用的結構體的實例。

struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

可以在 impl 塊中定義不以 self 作為參數的函數,稱為關聯函數,因為它們和結構體相關聯。即便如此它們也是函數而不是方法,因為它們并不作用于一個結構體的實例。例如:String::from 就是一個關聯函數。

關聯函數經常被用作返回一個結構體新實例的構造函數

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { length: size, width: size }
    }
}

使用結構體名和 :: 語法來調用這個關聯函數:比如 let sq = Rectangle::square(3); 。這個方法位于結構體的命名空間中: :: 語法用于關聯函數和模塊創建的命名空間。

枚舉

使用 enum 關鍵字聲明枚舉,可以將任意類型的數據放入枚舉成員中:例如字符串、數字類型或者結構體。甚至可以包含另一個枚舉!

枚舉同結構體一樣使用 impl 聲明其方法,方法體使用了 self 來調用方法的值。

Option 枚舉

Rust 并沒有很多其它語言中有的空值功能。不過它確實擁有一個可以編碼存在或不存在概念的枚舉。這個枚舉是 Option<T>,而且它定義于標準庫中,如下:

enum Option<T> {
    Some(T),
    None,
}

空值(Null) 是一個值,它代表沒有值。在有空值的語言中,變量總是這兩種狀態之一:空值和非空值。

Option<T> 已被 prelude 自動引用,不需要顯式導入它,它的成員也是如此,不需要 Option::前綴來直接使用SomeNone。即便如此 Option<T> 也仍是常規的枚舉,Some(T)None仍是Option<T>的成員。

如果使用 None 而不是 Some,需要告訴 Rust Option<T>是什么類型的,因為編譯器只通過None值無法推斷出Some成員的類型。

let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None; // 這里必須聲明類型

當有一個 Some 值時,我們就知道存在一個值,而這個值保存在 Some 中。當有個 None 值時,在某種意義上它跟空值是相同的意義:并沒有一個有效的值。

Option<T>T(這里T可以是任何類型)是不同的類型,編譯器不允許像一個被定義的有效的類型那樣使用 Option<T>。例如,這些代碼不能編譯,因為它嘗試將 Option<i8>i8相比:

let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // error no implementation for `i8 + Option<i8>`

當在 Rust 中擁有一個像i8這樣類型的值時,編譯器確保它總是有一個有效的值。我們可以自信使用而無需判空。只有當使用Option<i8>(或者任何用到的類型)是需要擔心可能沒有一個值,而編譯器會確保我們在使用值之前處理為空的情況。

match 運算符

match 允許將一個值與一系列的模式相比較并根據匹配的模式執行代碼。模式可由字面值、變量、通配符和許多其它內容構成。

match 關鍵字后跟一個任意類型的表達式,之后是 match 分支:一個模式和一些代碼。使用 => 運算符將模式和將要運行的代碼分開。每個分支相關聯的代碼是一個表達式,而表達式的結果值將作為整個 match 表達式的返回值。例如:

match coin {
    Coin::Penny => {
        println!("Lucky penny!");
        1
    },
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
}

Rust 中的匹配是窮盡的:必須窮舉到最后的可能性來使代碼有效

_ 通配符

_ 模式會匹配所有的值。通過將其放置于其它分支之后,將會匹配所有之前沒有指定的可能的值。如果希望匹配后不做任何處理可以這樣 _ =>(),

if let

if letmatch的一個語法糖,它當只匹配某一模式時執行代碼而忽略所有其它值。但這樣會失去match強制要求的窮盡性檢查。

if let Some(3) = some_u8_value {
    println!("three");
}

可以在if let中包含一個elseelse塊中的代碼與match表達式中的_分支塊中的代碼相同。

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

模塊

模塊(module)是一個包含函數或類型定義的命名空間,你可以選擇這些定義是能(公有)還是不能(私有)在其模塊外可見。一個模塊按照如下工作:

  • 使用mod關鍵字聲明模塊
  • 默認所有內容都是私有的(包括模塊自身)。可以使用pub關鍵字將其變成公有并在其命名空間外可見。
  • use關鍵字允許引入模塊、或模塊中的定義到作用域中以便于引用它們。

模塊文件系統的規則

  • 如果一個叫做foo的模塊沒有子模塊,應該將foo的聲明放入叫做 foo.rs 的文件中。
  • 如果一個叫做foo的模塊有子模塊,應該將foo的聲明放入叫做 foo/mod.rs 的文件中。模塊自身則應該使用mod關鍵字定義于父模塊的文件中。

可見性規則

  • 如果一個模塊是公有的,它能被任何父模塊訪問。
  • 如果一個模塊是私有的,它只能被當前模塊或其子模塊訪問。
  • 同位于根模塊,私有模塊的公有函數可以被訪問,私有模塊和私有函數都不可被訪問。
  • 除上述以外,私有模塊的公有函數,公有模塊的私有函數都不可被訪問。

我們創建的所有模塊都位于一個與 crate 同名的模塊內部。這個頂層的模塊被稱為 crate 的根模塊(root module)。

另外注意到即便在項目的子模塊中使用外部 crate,extern crate也應該位于根模塊(也就是 src/main.rssrc/lib.rs)。接著,在子模塊中,我們就可以像頂層模塊那樣引用外部 crate 中的項了。

使用 use 簡化導入

use 關鍵字的工作是縮短冗長的函數調用,通過將想要調用的函數所在的模塊引入到作用域中。例如:

pub mod a {
    pub mod series {
        pub mod of {
            pub fn nested_modules() {}
        }
    }
}

use a::series::of; // 每當想要引用of模塊時,不用使用完整的a::series::of路徑,直接使用of

fn main() {
    of::nested_modules();
}

use 關鍵字只將指定的模塊引入作用域;它并不會將其子模塊也引入。

可以將函數本身引入到作用域中,通過如下在use中指定函數的方式:

use a::series::of::nested_modules;

fn main() {
    nested_modules();
}

因為枚舉也像模塊一樣組成了某種命名空間,也可以使用use來導入枚舉的成員。對于任何類型的use語句,如果從一個命名空間導入多個項,可以使用大括號和逗號來列舉它們,像這樣:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

use TrafficLight::{Red, Yellow};

fn main() {
    let red = Red;
    let yellow = Yellow;
    let green = TrafficLight::Green;
}

為了一次導入某個命名空間的所有項,可以使用 * 語法。例如:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

use TrafficLight::*;

fn main() {
    let red = Red;
    let yellow = Yellow;
    let green = Green;
}

*被稱為 全局導入(glob),它會導入命名空間中所有可見的項。全局導入應該保守的使用:它們是方便的,但是也可能會引入多于你預期的內容從而導致命名沖突。

使用 super 訪問父模塊

使用 super 關鍵字獲取當前模塊的父模塊,如果是用::mod1::mod2 則要從根模塊開始列出整個路徑。

use 關鍵字是相對于根模塊開始的,可以通過 use super:child1 相對于父模塊開始。

通用集合類型

不同于內建的數組和元組類型,這些集合指向的數據是儲存在堆上的,這意味著數據的數量不必在編譯時就可知并且可以隨著程序的運行增長或縮小。

vector

Vec<T> 類型,也被稱為 vector。允許我們在一個單獨的數據結構中儲存多于一個值,它在內存中彼此相鄰的排列所有的值。vector 只能儲存相同類型的值。

使用 Vec::new 函數創建空的 vector:let v: Vec<i32> = Vec::new(); 由于沒有向這個 vector 中插入任何值,Rust 并不知道我們想要儲存什么類型的元素。所以需要添加類型注解。

更常見的做法是使用初始值來創建一個 vector,這樣可以省去類型注解。為了方便,Rust 提供了 vec! 宏。它會根據提供的值來創建新的vector,例如:let v = vec![1,2,3];

可以使用 push方法向vector添加元素,但必須聲明是 mut

let mut v = vec![];
v.push(1);

在 vector 的結尾增加新元素時,在沒有足夠空間將所有所有元素依次相鄰存放的情況下,可能會要求分配新內存并將舊的元素拷貝到新的空間中。這時,對之前元素的引用就指向了被釋放的內存,會引發錯誤,例如:

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // 這里的引用會引發錯誤,直接用 v[0] 則不會。
v.push(6);

還可以是用 pop 方法移除并返回 vector 的最后一個元素。

vector 在其離開作用域時會被釋放,其內容也會被丟棄。

讀取 vector 中的元素可以使用索引或get方法,如:

let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);

使用索引不能訪問不存在的元素,否則會造成 panic!,而使用get方法訪問不存在的元素會返回None,因為它返回的是Option<T>類型,要么是Some(T)要么是None

由于 vector 只能存儲相同類型的值,需要存儲不同類型的值時,可以使用枚舉,因為枚舉的成員都是枚舉類型。例如:

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

字符串

Rust 核心語言中事實上只有一種字符串類型:str 字符串 slice,它通常以被借用的形式出現——&str。它們是一些存儲在別處的 UTF-8 編碼字符串數據的引用。比如字符串字面值被存儲在程序的二進制輸出中,字符串 slice 也是如此。

稱作 String 的類型是由標準庫提供的,而沒有寫進核心語言部分, 它是可增長的、可變的、有所有權的、UTF-8編碼的字符串類型。當談到 Rust 的“字符串”時,它們通常指的是 String 和字符串 slice &str 類型,而不是其中一個。

String和字符串 slice 都是 UTF-8編碼的。

Rust 標準庫中還包含一系列其他字符串類型,比如 OsStringOsStrCStringCStr

新建字符串

使用 new()函數創建空字符串。let s = String::new();

可以使用 to_string方法創建有內容的字符串。它能用于任何實現了 Display trait 的類型,對于字符串字面值:

let data = "initial contents";
let s = data.to_string();
// 也可以直接對字符串字面值使用
let s = "initial contents".to_string();

還可以使用 String::from() 函數來從字符串字面值創建 String。等同于使用 to_stringlet s = String::from("initial contents");

更新字符串

String 的大小可以增長其內容也可以改變。

  • 使用push_str方法添加字符串 slice。
let mut s = String::from("foo");
let s2 = String::from("bar");
s.push_str("bar");
s.push_str(&s2) // 這里是&s2 而不是s2
  • 使用 push 方法添加字符。
let mut s = String::from("lo");
s.push('l');
  • 使用 + 運算符將兩個已知的字符串合并在一起,只能將&strString 相加,不能將兩個String值或兩個&str相加。
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
// let s3 = s1 + "wold";
let s3 = s1 + &s2; // s1 會發生轉移 之后將不能使用

  • 使用 format! 宏,將多個 String&str 合并在一起。
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);

索引字符串

Rust 的字符串 不支持索引。因為 String 是一個 Vec<u8> 的封裝。采用 UTF-8 編碼,不同的語言文字,一個字符串字節值的索引并不總是對應一個有效的 Unicode 標量值。Rust不得不檢查從字符串的開頭到索引位置的內容來確定這里有多少有效的字符,這就損害了性能,同時,為了避免返回意想不到的值造成意外的bug,Rust禁止使用索引。但如果確實需要的話可以使用字符串 slice。例如:

let s1 = String::from("tic");
let s2= "wold";
println!("{}", &s1[..1],&s2[1..2]); // 不要忘了 & 符號

遍歷字符串

  • chars 方法,操作單獨的 Unicode 標量值。
for c in "??????".chars() {
    println!("{}", c); // ? ? ?  ? ?  ?
}
  • bytes 方法返回每一個原始字節。
for b in "??????".bytes() {
    println!("{}", b); // 224 164 168 224 ...
}

HashMap

HashMap<K,V> 類型儲存了一個鍵類型 K 對應一個值類型 V 的映射。它通過一個 哈希函數(hashing function)來實現映射,決定如何將鍵和值放入內存中。

  • 使用 new 創建一個空的 HashMap,并使用 insert 來增加元素。
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

HashMap 是同質的:所有的鍵必須是相同類型,值也必須都是相同類型

  • 使用 collect 方法,結合 zip 方法,將 元祖 vector 轉換成 HashMap。
use std::collections::HashMap;

let teams  = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); // HashMap<_, _>類型注解是必要的

HashMap 和所有權

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value); // field_name field_value 被插入后所有權轉移到 map,之后不能使用這兩個綁定
// 如果將值的引用插入哈希 map,這些值本身將不會被移動進哈希 map。但是這些引用指向的值必須至少在哈希 map 有效時也是有效的

訪問 HashMap 中的值

  • 通過 get 方法并提供對應的鍵來從 HashMap 中獲取值:
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name); // 返回的是 Option<T>類型
  • 使用與 for 循環遍歷 HashMap 中的每一個鍵值對:
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (key, value) in &scores {
    println!("{}: {}", key, value); // 以任意順序打印出每一個鍵值對:
}

更新 HashMap

  • 使用 insert 插入一個鍵值對,如果之前有值,新值會代替舊值:
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);

println!("{:?}", scores); // {"Blue": 25}
  • 使用 entryor_insert 方法,如果存在就忽略,如果不存在就插入,or_insert 方法會返回這個鍵的值的一個可變引用(&mut V):
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50); // Blue 已存在 不會改變

println!("{:?}", scores);
  • 根據舊值更新一個值:
use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

println!("{:?}", map); // {"world": 2, "hello": 1, "wonderful": 1}

錯誤處理

Rust 將錯誤組合成兩個主要類別:可恢復錯誤(recoverable)和 不可恢復錯誤(unrecoverable)。可恢復錯誤 通常代表向用戶報告錯誤和重試操作是合理的情況,比如未找到文件。不可恢復錯誤 通常是 bug 的同義詞,比如嘗試訪問超過數組結尾的位置。

Rust 并沒有異常。對于可恢復錯誤有 Result<T, E> 值,對于不可恢復錯誤有 panic!

panic!宏

當執行這個宏時,程序會打印出一個錯誤信息,展開并清理棧數據,然后接著退出。出現這種情況的場景通常是檢測到一些類型的 bug 而且程序員并不清楚該如何處理它。

當出現 panic! 時,程序默認開始 展開(unwinding),Rust 會回溯棧并清理它遇到的每一個函數的數據,不過這個回溯并清理的過程有很多工作。如果需要最終發布的二進制文件越小越好,可以選擇:直接 終止(abort) ——不清理數據就退出程序。那么程序所使用的內存需要由操作系統來清理。通過在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',將展開切換為終止。例如:

[profile.release]
panic = 'abort'
  • 主動調用 panic!
fn main() {
    panic!("crash and burn");
}

運行程序將會出現類似這樣的輸出:

Compiling panic v0.1.0 (file:///projects/panic)
Finished debug [unoptimized + debuginfo] target(s) in 0.25 secs
    Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2 // 顯示錯誤信息及出現位置
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic.exe` (exit code: 101)
  • 因為代碼中的 bug 引起的別的庫中 panic!
fn main() {
    let v = vec![1, 2, 3];
    v[100];
}

運行程序會出現如下錯誤信息:

Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target\debug\panic.exe`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 11', C:\projects\rust\src\libcollections\vec.rs:1552 // 顯示別人代碼中 錯誤信息及位置
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: process didn't exit successfully: `target\debug\panic.exe` (exit code: 101)

上述情況可以設置 RUST_BACKTRACE=1來顯示更多信息:

  • cmd下,輸入 set RUST_BACKTRACE=1
  • power shell下,輸入 $env:RUST_BACKTRACE=1

之后再執行 cargo run,顯示如下信息:

Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target\debug\panic.exe`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 11', C:\projects\rust\src\libcollections\vec.rs:1552
stack backtrace:
   0: std::sys_common::backtrace::_print
             at C:\projects\rust\src\libstd\sys_common\backtrace.rs:71
   1: std::panicking::default_hook::{{closure}}
             at C:\projects\rust\src\libstd\panicking.rs:354
   2: std::panicking::default_hook
             at C:\projects\rust\src\libstd\panicking.rs:371
   3: std::panicking::rust_panic_with_hook
             at C:\projects\rust\src\libstd\panicking.rs:549
   4: std::panicking::begin_panic<collections::string::String>
             at C:\projects\rust\src\libstd\panicking.rs:511
   5: std::panicking::begin_panic_fmt
             at C:\projects\rust\src\libstd\panicking.rs:495
   6: std::panicking::rust_begin_panic
             at C:\projects\rust\src\libstd\panicking.rs:471
   7: core::panicking::panic_fmt
             at C:\projects\rust\src\libcore\panicking.rs:69
   8: core::panicking::panic_bounds_check
             at C:\projects\rust\src\libcore\panicking.rs:56
   9: collections::vec::{{impl}}::index<i32>
             at C:\projects\rust\src\libcollections\vec.rs:1552
  10: panic::main
             at .\src\main.rs:3 // 自己程序中引起錯誤的行
  11: panic_unwind::__rust_maybe_catch_panic
             at C:\projects\rust\src\libpanic_unwind\lib.rs:98
  12: std::rt::lang_start
             at C:\projects\rust\src\libstd\rt.rs:52
  13: main
  14: __scrt_common_main_seh
             at f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl:259
  15: BaseThreadInitThunk
error: process didn't exit successfully: `target\debug\panic.exe` (exit code: 101)

上面顯示了程序執行到目前位置所有被調用的函數的列表。從頭開始讀直到發現自己的文件。這就是問題的發源地。這一行往上是調用的代碼;往下則是被調用的代碼。這些行可能包含核心 Rust 代碼,標準庫代碼或用到的 crate 代碼。

Result

enum Result<T, E> {
    Ok(T),
    Err(E),
}

TE 是泛型類型參數: T 代表成功時返回的
Ok 成員中的數據的類型,而 E 代表失敗時返回的 Err 成員中的錯誤的類型。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt"); // 成功時,f的值是一個包含文件句柄的Ok實例;
                                    // 失敗時,f會是一個包含更多關于出現了何種錯誤信息的Err實例

    let f = match f {
        Ok(file) => file,
        // 如果因為文件不存在而失敗,創建文件并返回新文件的句柄
        // if error.kind() == ErrorKind::NotFound被稱作 match guard:
        // 它是一個進一步完善match分支模式的額外的條件。這個條件必須為真才能使分支的代碼被執行;
        // 否則,模式匹配會繼續并考慮match中的下一個分支。
        // 模式中的ref是必須的,這樣error就不會被移動到 guard 條件中而只是僅僅引用它
        // 在模式的上下文中,&匹配一個引用并返回它的值,而ref匹配一個值并返回一個引用。
        Err(ref error) if error.kind() == ErrorKind::NotFound => match File::create("hello.txt") {
            Ok(fc) => fc,
            Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
        },
        Err(error) => panic!("There was a problem opening the file: {:?}", error),
    };
}

File::open 函數的返回值類型是 Result<T, E>,成功值的類型 std::fs::File,它是一個文件句柄;失敗值的類型是 std::io::Error,它是標準庫中提供的結構體,這個結構體有一個返回 io::ErrorKind 值的 kind 方法可供調用。io::ErrorKind 是一個標準庫提供的枚舉,它的成員對應 io 操作可能導致的不同錯誤類型。ErrorKind::NotFound 代表嘗試打開的文件不存在。

Result 枚舉和其成員也被導入到了 prelude 中,所以就不需要 OkErr 之前指定 Result::

失敗時 panic 的捷徑:unwrap和expect

Result<T, E> 類型定義了很多輔助方法來處理各種情況。

  • unwrap: 如果 Result 值是 Ok,返回 Ok 中的值,如果是 Err會自動調用 panic!
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}
  • expect: 可以在參數中自定義要顯示的錯誤信息,并在自動調用 panic! 時,顯示在錯誤信息中,有助于追蹤 panic 的根源。
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

傳播錯誤

當編寫一個其實現會調用一些可能會失敗的操作的函數時,除了在這個函數中處理錯誤外,還可以選擇讓調用者知道這個錯誤并決定該如何處理,這被稱為 傳播(propagating)錯誤。簡便的專用語法:?,可以在 ? 之后直接使用鏈式方法調用來進一步縮短代碼:

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

? 只能用于返回值類型為 Result 的函數。

示例、代碼原型和測試:非常適合 panic

當編寫一個示例來展示一些概念時,在擁有健壯的錯誤處理代碼的同時也會使得例子不那么明確。例如,調用一個類似 unwrap 這樣可能 panic! 的方法可以被理解為一個你實際希望程序處理錯誤方式的占位符,它根據其余代碼運行方式可能會各不相同。

unwrapexpect 方法在原型設計時非常方便,在決定該如何處理錯誤之前。它們在代碼中留下了明顯的記號,以便準備使程序變得更健壯時作為參考。
如果方法調用在測試中失敗了,我們希望這個測試都失敗,即便這個方法并不是需要測試的功能。因為 panic! 是測試如何被標記為失敗的,調用 unwrapexpect 都是非常有道理的。

泛型

Rust 通過在編譯時進行泛型代碼的 單態化(monomorphization)來保證效率。單態化是一個將泛型代碼轉變為實際放入的具體類型的特定代碼的過程。這意味著在使用泛型時沒有運行時開銷;當代碼運行,它的執行效率就跟好像手寫每個具體定義的重復代碼一樣。

在函數定義中使用泛型

定義函數時可以在函數簽名的參數數據類型和返回值中使用泛型。

fn largest<T>(list: &[T]) -> T {}

結構體定義中的泛型

使用 <> 來定義擁有一個或多個泛型參數類型字段的結構體。

struct Point<T, U> {
    x: T,
    y: U,
}

枚舉定義中的泛型數據類型

enum Result<T, E> {
    Ok(T),
    Err(E),
}

方法定義中的枚舉數據類型

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

注意必須在 impl 后面聲明 T,這樣就可以在 Point<T> 上實現的方法中使用它了。

結構體定義中的泛型類型參數并不總是與結構體方法簽名中使用的泛型是同一類型

struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

注意泛型參數 TU 聲明于 impl 之后,因為他們于結構體定義相對應。而泛型參數 VW 聲明于 fn mixup 之后,因為他們只是相對于方法本身的。

trait

使用 trait 關鍵字來定義一個 trait,后面是 trait 的名字。在大括號中聲明描述實現這個 trait 的類型所需要的行為的方法簽名。在方法簽名后跟分號而不是在大括號中提供其實現。接著每一個實現這個 trait 的類型都需要提供其自定義行為的方法體,編譯器也會確保任何實現這個 trait 的類型都擁有與這個簽名的定義完全一致的方法。

trait 體中可以有多個方法,一行一個方法簽名且都以分號結尾。

pub trait Summarizable {
    fn summary(&self) -> String;
}

實現 trait

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summarizable for Tweet {
    fn summary(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

如果要實現外部定義的 trait 需要先將其導入作用域。不允許對外部類型實現外部 trait,可以對外部類型實現自定義的 trait,也可以對自定義類型上實現外部 trait。這個限制稱為 孤兒規則(orphan rule),即其父類型不存在。

默認實現

有時為 trait 中的某些或全部提供默認的行為,而不是在每個類型的每個實現中都定義自己的行為是很有用的。這樣當為某個特定類型實現 trait 時,可以選擇保留或重載每個方法的默認行為。

pub trait Summarizable {
    fn summary(&self) -> String {
        String::from("(Read more...)")
    }
}

使用默認實現,可以指定一個空的 impl 塊:impl Summarizable for NewsArticle {}

重載實現

重載一個默認實現的語法與實現沒有默認實現的 trait 方法時完全一樣的。默認實現允許調用相同 trait 中的其他方法,哪怕這些方法沒有默認實現。通過這種方法,trait 可以實現很多有用的功能而只需實現一小部分特定內容。

pub trait Summarizable {
    fn author_summary(&self) -> String;

    fn summary(&self) -> String {
        format!("(Read more from {}...)", self.author_summary())
    }
}

impl Summarizable for Tweet {
    fn author_summary(&self) -> String {
        format!("@{}", self.username)
    }
}

注意在重載過的實現中調用默認實現是不可能的

trait bounds

可以對泛型類型參數使用 trait,從而限制泛型不再適用于任何類型,編譯器會確保其被限制為那些實現了特定 trait 的類型,由此泛型就會擁有我們希望其類型所擁有的功能。這被稱為指定泛型的 trait bounds

pub fn notify<T: Summarizable>(item: T) {
    println!("Breaking news! {}", item.summary());
}

可以通過 + 來為泛型指定多個 trait bounds: <T: Summarizable + Display>。在函數名和參數列表之間的尖括號中指定很多的 trait bound 信息將是難以閱讀的,所以將其移動到函數簽名后的 where 從句中。

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

生命周期與引用有效性

Rust 中的每一個 引用 都有其生命周期,也就是引用保持有效的作用域。生命周期的主要目標是避免懸垂引用,它會導致程序引用了并非其期望引用的數據。

未初始化變量不能被使用

借用檢查器 用來比較作用域來確保所有的借用都是有效的。

生命周期注解語法

生命周期注解并不改變任何引用的生命周期的長短。與當函數簽名中指定了泛型類型參數后就可以接受任何類型一樣,當指定了泛型生命周期后函數也能接受任何生命周期的引用。生命周期注解所做的就是將多個引用的生命周期聯系起來。

生命周期參數名稱必須以單引號號(')開頭。生命周期參數的名稱通常全是小寫,而且類似于泛型類型,其名稱通常非常短。'a 是大多數人默認使用的名稱。生命周期參數注解位于引用的 & 之后,并有一個空格來將引用類型與生命周期注解分隔開。

&i32        // 沒有生命周期的引用
&'a i32     // 有生命周期的引用
&'a mut i32 // 有生命周期的可變引用

生命周期注解告訴 Rust 多個引用的泛型生命周期參數如何相互聯系。如果函數有一個生命周期 'ai32 的引用的參數 first,還有另一個同樣是生命周期 'ai32 的引用的參數 second,這兩個生命周期注解有相同的名稱意味著 firstsecond 必須與這相同的泛型生命周期存在得一樣久。

函數簽名中的生命周期注解

泛型生命周期參數需要聲明在函數名和參數列表間的尖括號中。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

通過在函數簽名中指定生命周期參數,不會改變任何參數或返回值的生命周期,任何不堅持這個協議的類型都將被借用檢查器拒絕。

當從函數返回一個引用,返回值的生命周期參數需要與一個參數的生命周期參數相匹配。如果返回的引用沒有指向任何一個參數,那么唯一的可能就是它指向一個函數內部創建的值,它將會是一個懸垂引用,因為它將會在函數結束時離開作用域。

生命周期語法是關于如何聯系函數不同參數和返回值的生命周期的。一旦他們形成了某種聯系,Rust 就有了足夠的信息來允許內存安全的操作并阻止會產生懸垂指針亦或是違反內存安全的行為。

結構體定義中的生命周期注解

可以定義存放引用的結構體,但需要為結構體定義中的每一個引用添加生命周期注解。

struct ImportantExcerpt<'a> {
    part: &'a str,
}

生命周期省略

函數或方法的參數的生命周期被稱為 輸入生命周期(input lifetimes),而返回值的生命周期被稱為 輸出生命周期(output lifetimes)。

譯器用于判斷引用何時不需要明確生命周期注解的規則。第一條規則適用于輸入生命周期,而后兩條規則則適用于輸出生命周期。如果編譯器檢查完這三條規則并仍然存在沒有計算出生命周期的引用,編譯器將會停止并生成錯誤。

  1. 對于輸入生命周期,每一個引用的參數都有它自己的生命周期參數。每一個被省略的函數參數成為一個不同的生命周期參數。
  2. 如果只有一個輸入生命周期參數,它被賦給所有輸出聲明周期參數。
  3. 如果方法有多個輸入生命周期參數,其中之一因為方法的緣故是 &self&mut self,那么 self 的生命周期被賦給所有輸出生命周期參數。這使得方法寫起來更簡潔。

方法定義中的生命周期注解

實現方法時,結構體字段的生命周期必須總是在 impl 關鍵字之后聲明并在結構體名稱之后被使用,因為這些生命周期是結構體類型的一部分。

impl 塊里的方法簽名中,引用可能與結構體字段中的引用相關聯,也可能是獨立的。另外,生命周期省略規則也經常讓我們無需在方法簽名中使用生命周期注解。

struct ImportantExcerpt<'a> {
   part: &'a str,
}


impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

靜態生命周期

'static 生命周期存活于整個程序期間。所有的字符串字面值都擁有 'static 生命周期。let s: &'static str = "I have a static lifetime.";

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容