Rust 語法梳理與總結(上)
楔子
關於 Rust 的基礎知識我們已經介紹一部分了,下面來做一個總結。因爲 Rust 是一門難度非常高的語言,在學習完每一個階段之後,對學過的內容適當總結一下是很有必要的。
那麼下面就開始吧,將以前說過的內容再總結一遍,並且在這個過程中還會補充一些之前遺漏的內容。
原生類型
首先是 Rust 的原生類型,原生類型包含標量類型和複合類型。
另外在 Rust 裏面,空元組也被稱爲單元類型。
在聲明變量的時候,可以顯式地指定類型,舉個例子:
fn main(){
let x: i64 = 123;
let y: bool = true;
let z: [u8; 3] = [1, 2, 3];
println!("x = {}", x);
println!("y = {}", y);
println!("z = {:?}", z);
/*
x = 123
y = true
z = [1, 2, 3]
*/
}
另外數字比較特殊,還可以通過後綴指定類型。
fn main(){
// u8 類型
let x = 123u8;
// f64 類型
let y = 3.14f64;
println!("x = {}", x);
println!("y = {}", y);
/*
x = 123
y = 3.14
*/
}
如果沒有顯式指定類型,也沒有後綴,那麼整數默認爲 i32,浮點數默認爲 f64。
fn main(){
// 整數默認爲 i32
let x = 123;
// 浮點數默認爲 f64
let y = 3.14;
}
最後 Rust 還有一個自動推斷功能,會結合上下文推斷數值的類型。
fn main(){
// 本來默認 x 爲 i32,y 爲 f64
let x = 123;
let y = 3.14;
// 但是這裏我們將 x, y 組合成元組賦值給了 t
// 而 t 是 (u8, f32),所以 Rust 會結合上下文
// 將 x 推斷成 u8,將 y 推斷成 f32
let t: (u8, f32) = (x, y);
}
但如果我們在創建 x 和 y 的時候顯式地規定了類型,比如將 x 聲明爲 u16,那麼代碼就不合法了。因爲 t 的第一個元素是 u8,但傳遞的 x 卻是 u16,此時就會報錯,舉個例子:
Rust 對類型的要求非常嚴格,即便都是數值,類型不同也不能混用。那麼這段代碼應該怎麼改呢?
fn main(){
let x = 123u16;
let y = 3.14;
let t: (u8, f32) = (x as u8, y);
}
通過 as 關鍵字,將 x 轉成 u8 就沒問題了。
然後我們上面創建的整數都是十進制,如果在整數前面加上 0x, 0o, 0b,還可以創建十六進制、八進制、二進制的整數。並且在數字比較多的時候,爲了增加可讀性,還可以使用下劃線進行分隔。
fn main(){
let x = 0xFF;
let y = 0o77;
// 數字較多時,使用下劃線分隔
let z = 0b1111_1011;
// 以 4 個數字爲一組,這樣最符合人類閱讀
// 但 Rust 語法則沒有此要求,我們可以加上任意數量的下劃線
let z = 0b1_1_1_1_______10______1_1;
println!("x = {}, y = {}, z = {}", x, y, z);
/*
x = 255, y = 63, z = 251
*/
}
至於算術運算、位運算等操作,和其它語言都是類似的,這裏不再贅述。
元組
再來單獨看看元組,元組是一個可以包含各種類型值的組合,使用括號來創建,比如 (T1, T2, ...),其中 T1、T2 是每個元素的類型。函數可以使用元組來返回多個值,因爲元組可以擁有任意多個值。
Python 的多返回值,本質上也是返回了一個元組。
fn main(){
// t 的類型就是 (i32, f64, u8, f32)
let t = (12, 3.14, 33u8, 2.71f32);
// 當然你也可以這麼做
let t: (i32, f64, u8, f32) = (12, 3.14, 33, 2.71);
// 但下面的做法是非法的
// 因爲 t 的第一個元素要求是 i32,而我們傳遞的 u8
/*
let t: (i32, f64) = (12u8, 3.14)
*/
// 應該改成這樣
/*
let t: (i32, f64) = (12i32, 3.14)
*/
// 只不過這種做法有點多餘,因爲 t 已經規定好類型了
// 所以沒必要寫成 12i32,直接寫成 12 就好
}
元組裏面的元素個數是固定的,類型也是固定的,但是每個元素之間可以是不同的類型。
fn main(){
// 此時 t 的類型就會被推斷爲
// ((i32, f64, i32), (i32, u16), i32)
let t = ((1, 2.71, 3), (1, 2u16), 33);
}
然後是元組的打印,有兩種方式。
fn main(){
let t = (1, 22, 333);
// 元組打印需要使用 "{:?}"
println!("{:?}", t);
// 或者使用 "{:#?}" 美化打印
println!("{:#?}", t);
/*
(1, 22, 333)
(
1,
22,
333,
)
*/
}
有了元組之後,還可以對其進行解構。
fn main() {
let t = (1, 3.14, 7u16);
// 將 t 裏面的元素分別賦值給 x、y、z
// 這個過程稱爲元組的結構
// 變量多元賦值也是通過這種方式實現的
let (x, y, z) = t;
println!("x = {}, y = {}, z = {}", x, y, z);
// x = 1, y = 3.14, z = 7
// 當然我們也可以通過索引,單獨獲取元組的某個元素
// 只不過方式是 t.索引,而不是 t[索引]
let x = t.0;
}
再補充一點,創建元組的時候使用的是小括號,但我們知道小括號也可以起到一個限定優先級的作用。因此當元組只有一個元素的時候,要顯式地在第一個元素後面加上一個逗號。
fn main() {
// t1 是一個 i32,因爲 (1) 等價於 1
let t1 = (1);
// t2 纔是元組,此時 t2 是 (i32,) 類型
let t2 = (1,);
println!("t1 = {}", t1);
println!("t2 = {:?}", t2);
/*
t1 = 1
t2 = (1,)
*/
// 同樣的,當指定類型的時候也是如此
// 如果寫成 let t3: (i32),則等價於 let t3: i32
let t3: (i32,) = (1,);
}
至於將元組作爲函數參數和返回值,也是同樣的用法,這裏就不贅述了。
數組和切片
數組(array)是一組擁有相同類型 T 的對象的集合,在內存中是連續存儲的,所以數組不僅要求長度固定,每個元素類型也必須一樣。數組使用中括號來創建,且它們的大小在編譯時會被確定。
fn main() {
// 數組的類型被標記爲 [T; length]
// 其中 T 爲元素類型,length 爲數組長度
let arr: [u8; 5] = [1, 2, 3, 4, 5];
println!("{:?}", arr);
/*
[1, 2, 3, 4, 5]
*/
// 不指定類型,可以自動推斷出來
// 此時會被推斷爲 [i32; 5]
let arr = [1, 2, 3, 4, 5];
// Rust 數組的長度也是類型的一部分
// 所以下面的 arr1 和 arr2 是不同的類型
let arr1 = [1, 2, 3]; // [i32; 3] 類型
let arr2 = [1, 2, 3, 4]; // [i32; 4] 類型
// 所以 let arr1: [i32; 4] = [1, 2, 3] 是不合法的
// 因爲聲明的類型是 [i32; 4],但傳遞的值的類型是 [i32; 3]
}
如果創建的數組所包含的元素都是相同的,那麼有一種簡便的創建方式。
fn main() {
// 有 5 個元素,且元素全部爲 3
let arr = [3; 5];
println!("{:?}", arr);
/*
[3, 3, 3, 3, 3]
*/
}
然後是元素訪問,這個和其它語言一樣,也是基於索引。
fn main() {
let arr = [1, 2, 3];
println!("arr[1] = {}", arr[1]);
/*
arr[1] = 2
*/
// 如果想修改數組的元素,那麼數組必須可變
// 無論是將新的數組賦值給 arr
// 還是通過 arr 修改當前數組內部的值
// 都要求數組可變,其它數據結構也是如此
let mut arr = [1, 2, 3];
// 修改當前數組的元素,要求 arr 可變
arr[1] = 222;
println!("arr = {:?}", arr);
/*
arr = [1, 222, 3]
*/
// 將一個新的數組綁定在 arr 上
// 也要求 arr 可變
arr = [2, 3, 4];
println!("arr = {:?}", arr);
/*
arr = [2, 3, 4]
*/
}
說完了數組,再來說一說切片(slice)。切片允許我們對數組的某一段區間進行引用,而無需引用整個數組。
fn main() {
let arr = [1, 2, 3, 4, 5, 6];
let slice = &arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
println!("{}", slice[1]); // 4
}
我們來畫一張圖描述一下:
這裏必須要區分一下切片和切片引用,首先代碼中的 arr[2..5] 是一個切片,由於截取的範圍不同,那麼切片的長度也不同。所以切片它不能夠分配在棧上,因爲棧上的數據必須有一個固定的、且在編譯期就能確定的大小,而切片的長度不固定,那麼大小也不固定,因此它只能分配在堆上。
既然分配在堆上,那麼就不能直接使用它,必須要通過引用。在 Rust 中,凡是堆上的數據,都是通過棧上的引用訪問的,切片也不例外,而 &a[2..5] 便是切片引用。
切片引用是一個寬指針,裏面存儲的是一個指針和一個長度,因此它不光可以是數組的切片,字符串也是可以的。
可能有人好奇 &arr[2..5] 和 &arr 有什麼區別?首先在變量前面加上 & 表示獲取它的引用,並且是不可變引用,而加上 &mut,則表示獲取可變引用。注意:這裏的可變引用中的可變兩個字,它指的不是引用本身是否可變,它描述的是能否通過引用去修改指向的值。
因此 &arr 表示對整個數組的引用,&arr[2..5] 表示對數組某個片段的引用。當然如果截取的片段是整個數組,也就是 &arr[..],那麼兩者是等價的。
然後再來思考一個問題,我們能不能通過切片引用修改底層數組呢?答案是可以的,只是對我們上面那個例子來說不可以。因爲上面例子中的數組是不可變的,所以我們需要聲明爲可變。
fn main() {
// 最終修改的還是數組,因此數組可變是前提
let mut arr = [1, 2, 3, 4, 5, 6];
// 但數組可變還不夠,引用也要是可變的
// 注意:只有當變量是可變的,才能拿到它的可變引用
// 因爲可變引用的含義就是:允許通過引用修改指向的值
// 但如果變量本身不可變的話,那可變引用還有啥意義呢?
// 因此 Rust 不允許我們獲取一個'不可變變量'的可變引用
let slice = &mut arr[2..5];
// 通過引用修改指向的值
slice[0] = 11111;
println!("{:?}", arr);
/*
[1, 2, 11111, 4, 5, 6]
*/
// 變量不可變,那麼只能拿到它的不可變引用
// 而變量可變,那麼不可變引用和可變引用,均可以獲取
// 下面的 slice 就是不可變引用
let slice = &arr[2..5];
// 此時只能獲取元素,不能修改元素
// 因爲'不可變引用'不支持通過引用去修改值
}
所以要想通過引用去修改值,那麼不僅變量可變,還要獲取它的可變引用。
然後切片引用的類型 &[T],由於數組是 i32 類型,所以這裏就是 &[i32]。
fn main() {
let mut arr = [1, 2, 3, 4, 5, 6];
// 切片的不可變引用
let slice: &[i32] = &arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
// 切片的可變引用
let slice: &mut [i32] = &mut arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
// 注意這裏的 slice 現在是可變引用,但它本身是不可變的
// 也就是說我們沒有辦法將一個別的切片引用賦值給它
// slice = &mut arr[2..6],這是不合法的
// 如果想這麼做,那麼 slice 本身也要是可變的
let mut slice: &mut [i32] = &mut arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
// 此時是允許的
slice = &mut arr[2..6];
println!("{:?}", slice); // [3, 4, 5, 6]
}
以上便是 Rust 的切片,當然我們不會直接使用切片,而是通過切片的引用。
自定義類型
Rust 允許我們通過 struct 和 enum 兩個關鍵字來自定義類型:
-
struct:定義一個結構體;
-
enum:定義一個枚舉;
而常量可以通過 const 和 static 關鍵字來創建。
結構體
結構體有 3 種類型,分別是 C 風格結構體、元組結構體、單元結構體。先來看後兩種:
// 不帶有任何字段,一般用於 trait
struct Unit;
// 元組結構體,相當於給元組起了個名字
struct Color(u8, u8, u8);
fn main() {
// 單元結構體實例
let unit = Unit{};
// 元組結構體實例
// 可以看到元組結構體就相當於給元組起了個名字
let color = Color(255, 255, 137);
println!(
"r = {}, g = {}, b = {}",
color.0, color.1, color.2
); // r = 255, g = 255, b = 137
// 然後是元組結構體的解構
let Color(r, g, b) = color;
println!("{} {} {}",
r, g, b); // 255 255 137
}
注意最後元組結構體實例的解構,普通元組的類型是 (T, ...),所以在解構的時候通過 let (變量, ...)。但元組結構體是 Color(T, ...),所以解構的時候通過 let Color(變量, ...)。
再來看看 C 風格的結構體。
// C 風格結構體
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
// 結構體也可以嵌套
#[derive(Debug)]
struct Rectangle {
// 矩形左上角和右下角的座標
top_left: Point,
bottom_right: Point,
}
fn main() {
let p1 = Point { x: 3, y: 5 };
let p2 = Point { x: 6, y: 10 };
// 訪問結構體字段,通過 . 操作符
println!("{}", p2.x); // 6
// 以 Debug 方式打印結構體實例
println!("{:?}", p1); // Point { x: 3, y: 5 }
println!("{:?}", p2); // Point { x: 6, y: 10 }
// 基於 Point 實例創建 Rectangle 實例
let rect = Rectangle {
top_left: p1,
bottom_right: p2,
};
// 計算矩形的面積
println!(
"area = {}",
(rect.bottom_right.y - rect.top_left.y) *
(rect.bottom_right.x - rect.top_left.x)
) // area = 15
}
最後說一下 C 風格結構體的解構:
struct Point {
x: i32,
y: f64,
}
fn main() {
let p = Point { x: 3, y: 5.2 };
// 用兩個變量保存 p 的兩個成員值,可以這麼做
// 我們用到了元組,因爲多元賦值本質上就是元組的解構
let (a, b) = (p.x, p.y);
// 或者一個一個賦值也行
let a = p.x;
let b = p.y;
// 結構體也支持解構
// 將 p.x 賦值給變量 a,將 p.y 賦值給變量 b
let Point { x: a, y: b } = p;
println!("a = {}, b = {}",
a, b); // a = 3, b = 5.2
// 如果賦值的變量名,和結構體成員的名字相同
// 那麼還可以簡寫,比如這裏要賦值的變量也叫 x、y
// 以下寫法和 let Point { x: x, y: y } = p 等價
let Point { x, y } = p;
println!("x = {}, y = {}",
x, y); // x = 3, y = 5.2
}
最後,如果結構體實例想改變的話,那麼也要聲明爲 mut。
枚舉
enum 關鍵字允許創建一個從數個不同取值中選其一的枚舉類型。
enum Cell {
// 成員可以是單元結構體
NULL,
// 也可以是元組結構體
Integer(i64),
Floating(f64),
DaysSales(u32, u32, u32, u32, u32),
// 普通結構體,或者說 C 風格結構體
TotalSales {cash: u32, currency: &'static str}
}
fn deal(c: Cell) {
match c {
Cell::NULL => println!("空"),
Cell::Integer(i) => println!("{}", i),
Cell::Floating(f) => println!("{}", f),
Cell::DaysSales(mon, tues, wed, thur, fri) => {
println!("{} {} {} {} {}",
mon, tues, wed, thur, fri)
},
Cell::TotalSales { cash, currency } => {
println!("{} {}", cash, currency)
}
}
}
fn main() {
// 枚舉的任何一個成員,都是枚舉類型
let c1: Cell = Cell::NULL;
let c2: Cell = Cell::Integer(123);
let c3: Cell = Cell::Floating(3.14);
let c4 = Cell::DaysSales(101, 111, 102, 93, 97);
let c5 = Cell::TotalSales {
cash: 504, currency: "USD"};
deal(c1); // 空
deal(c2); // 123
deal(c3); // 3.14
deal(c4); // 101 111 102 93 97
deal(c5); // 504 USD
}
所以當你要保存的數據的類型不確定,但屬於有限的幾個類型之一,那麼枚舉就特別合適。另外枚舉在 Rust 裏面佔了非常高的地位,像空值處理、錯誤處理都用到了枚舉。
然後是起別名,如果某個枚舉的名字特別長,那麼我們可以給該枚舉類型起個別名。當然啦,起別名不僅僅針對枚舉,其它類型也是可以的。
enum GetElementByWhat {
Id(String),
Class(String),
Tag(String),
}
fn main() {
// 我們發現這樣寫起來特別的長
let ele = GetElementByWhat::Id(String::from("submit"));
// 於是可以起個別名
type Element = GetElementByWhat;
let ele = Element::Id(String::from("submit"));
}
給類型起的別名應該遵循駝峯命名法,起完之後就可以當成某個具體的類型來用了。但要注意的是,類型別名並不能提供額外的類型安全,因爲別名不是新的類型。
除了起別名之外,我們還可以使用 use 關鍵字直接將枚舉成員引入到當前作用域。
enum GetElementByWhat {
Id(String),
Class(String),
Tag(String),
}
fn main() {
// 將 GetElementByWhat 的 Id 成員引入到當前作用域
use GetElementByWhat::Id;
let ele = Id(String::from("submit"));
// 也可以同時引入多個
// 這種方式和一行一行寫是等價的
use GetElementByWhat::{Class, Tag};
// 如果你想全部引入的話,也可以使用通配符
use GetElementByWhat::*;
}
然後 enum 也可以像 C 語言的枚舉類型一樣使用。
// 這些枚舉成員都有隱式的值
// Zero 等於 0,one 等於 1,Two 等於 2
enum Number {
Zero,
One,
Two,
}
fn main() {
// 既然是隱式的,就說明不能直接用
// 需要顯式地轉化一下
println!("Zero is {}", Number::Zero as i32);
println!("One is {}", Number::One as i32);
/*
Zero is 0
One is 1
*/
let two = Number::Two;
match two {
Number::Zero => println!("Number::Zero"),
Number::One => println!("Number::One"),
Number::Two => println!("Number::Two"),
}
/*
Number::Two
*/
// 也可以轉成整數
match two as i32 {
0 => println!("{}", 0),
1 => println!("{}", 1),
2 => println!("{}", 2),
// 雖然我們知道轉成整數之後
// 可能的結果只有 0、1、2 三種,但 Rust 不知道
// 所以還要有一個默認值
_ => unreachable!()
}
/*
2
*/
}
既然枚舉成員都有隱式的值,那麼可不可以有顯式的值呢?答案是可以的。
// 當指定值的時候,值必須是 isize 類型
enum Color {
R = 125,
G = 223,
B,
}
fn main() {
println!("R = {}", Color::R as u8);
println!("G = {}", Color::G as u8);
println!("B = {}", Color::B as u8);
/*
R = 125
G = 223
B = 224
*/
}
枚舉的成員 B 沒有初始值,那麼它默認是上一個成員的值加 1。但需要注意的是,如果想實現具有 C 風格的枚舉,那麼必須滿足枚舉裏面的成員都是單元結構體。
// 這個枚舉是不合法的
// 需要將 B(u8) 改成 B
enum Color {
R,
G,
B(u8),
}
還是比較簡單的。
常量
Rust 的常量,可以在任意作用域聲明,包括全局作用域。
// Rust 的常量名應該全部大寫
// 並且聲明的時候必須提供類型,否則編譯錯誤
const AGE: u16 = 17;
// 注意:下面這種方式不行
// 因爲這種方式本質上還是在讓 Rust 做推斷
// const AGE = 17u16;
fn main() {
// 常量可以同時在全局和函數里面聲明
// 但變量只能在函數里面
const NAME: &str = "komeiji satori";
println!("NAME = {}", NAME);
println!("AGE = {}", AGE);
/*
NAME = komeiji satori
AGE = 17
*/
}
注意:常量接收的必須是在編譯期間就能確定、且不變的值,我們不能把一個運行時才能確定的值綁定在常量上。
fn count () -> i32 {
5
}
fn main() {
// 合法,因爲 5 是一個編譯期間可以確定的常量
const COUNT1: i32 = 5;
// 下面也是合法的,像 3 + 2、4 * 8 這種,雖然涉及到了運算
// 但運算的部分都是常量,在編譯期間可以計算出來
// 所以會將 3 + 2 換成 5,將 4 * 8 換成 32
// 這個過程有一個專用術語,叫做常量摺疊
const COUNT2: i32 = 3 + 2;
// 但下面不行,count() 是運行時執行的
// 我們不能將它的返回值綁定在常量上
// const COUNT: i32 = count();
// 再比如數組,數組的長度也必須是常量,並且是 usize 類型
const LENGTH: usize = 5;
let arr: [i32; LENGTH] = [1, 2, 3, 4, 5];
// 但如果將 const 換成 let 就不行了
// 因爲數組的長度是常量,而 let 聲明的是變量
// 因此以下代碼不合法
/*
let LENGTH: usize = 5;
let arr: [i32; LENGTH] = [1, 2, 3, 4, 5];
*/
}
另外我們使用 let 可以聲明多個同名變量,這在 Rust 裏面叫做變量的隱藏。但常量不行,常量的名字必須是唯一的,而且也不能和變量重名。
除了 const,還有一個 static,它聲明的是靜態變量。但它的生命週期和常量是等價的,都貫穿了程序執行的始終。
// 靜態變量在聲明時同樣要顯式指定類型
static AGE: u8 = 17;
// 常量是不可變的,所以它不可以使用 mut 關鍵字
// 即 const mut xxx 是不合法的,但 static 可以
// 因爲 static 聲明的是變量,只不過它是靜態的
// 存活時間和常量是相同,都和執行的程序共存亡
static mut NAME: &str = "satori";
fn main() {
// 靜態變量也可以在函數內部聲明和賦值
static ADDRESS: &str = "じれいでん";
println!("AGE = {}", AGE);
println!("ADDRESS = {}", ADDRESS);
/*
AGE = 17
ADDRESS = じれいでん
*/
// 需要注意:靜態變量如果聲明爲可變
// 那麼在多線程的情況下可能造成數據競爭
// 因此使用的時候,需要放在 unsafe 塊裏面
unsafe {
NAME = "koishi";
println!("NAME = {}", NAME);
/*
NAME = koishi
*/
}
}
注意裏面用到了 unsafe ,關於啥是 unsafe 我們後續再聊,總之靜態變量我們一般很少會聲明爲可變。
變量綁定
接下來複習一下變量綁定,給變量賦值在 Rust 裏面有一個專門的說法:將值綁定到變量上。都是一個意思,我們理解就好。
fn main() {
// 綁定操作通過 let 關鍵字實現
// 將 u8 類型的 17 綁定在變量 age 上
let age = 17u8;
// 將 age 拷貝給 age2
let age2 = age;
}
如果變量聲明瞭但沒有使用,Rust 會拋出警告,我們可以在沒有使用的變量前面加上下劃線,來消除警告。
可變變量
變量默認都是不可變的,我們可以在聲明的時候加上 mut 關鍵字讓其可變。
#[derive(Debug)]
struct Color {
R: u8,
G: u8,
B: u8,
}
fn main() {
let c = Color{R: 155, G: 137, B: 255};
// 變量 c 的前面沒有 mut,所以它不可變
// 我們不可以對 c 重新賦值,也不可以修改 c 裏的成員值
// 如果想改變,需要使用 let mut 聲明
let mut c = Color{R: 155, G: 137, B: 255};
println!("{:?}", c);
/*
Color { R: 155, G: 137, B: 255 }
*/
// 聲明爲 mut 之後,我們可以對 c 重新賦值
c = Color{R: 255, G: 52, B: 102};
println!("{:?}", c);
/*
Color { R: 255, G: 52, B: 102 }
*/
// 當然修改 c 的某個成員值也是可以的
c.R = 0;
println!("{:?}", c);
/*
Color { R: 0, G: 52, B: 102 }
*/
}
所以要改變變量的值有兩種方式:
-
1)給變量賦一個新的值,這是所有變量都支持的,比如 let mut t = (1, 2),如果想將第一個元素改成 11,那麼 t = (11, 2) 即可;
-
2)針對元組、數組、結構體等,如果你熟悉 Python 的話,會發現這類似於 Python 裏的可變對象。也就是不賦一個新的值,而是對當前已有的值進行修改,比如 let mut t = (1, 2),如果想將第一個元素改成 11,那麼 t.0 = 11 即可。
但不管是將變量的值整體替換掉,還是對已有的值進行修改,本質上都是在改變變量的值。如果想改變,那麼變量必須聲明爲 mut。
作用域和隱藏
綁定的變量都有一個作用域,它被限制只能在一個代碼塊內存活,其中代碼塊是一個被大括號包圍的語句集合。
fn main() {
// 存活範圍是整個 main 函數
let name = "古明地覺";
{
// 新的作用域,裏面沒有 name 變量
// 那麼會從所在的外層作用域中尋找
println!("{}", name); // 古明地覺
// 創建了新的變量
let name = "古明地戀";
let age = 16;
println!("{}", name); // 古明地戀
println!("{}", age); // 16
}
// 再次打印 name
println!("{}", name); // 古明地覺
// 但變量 age 已經不存在了
// 外層作用域創建的變量,內層作用域也可以使用
// 但內層作用域創建的變量,外層作用域不可以使用
}
我們上面創建了兩個 name,但它們是在不同的作用域,所以彼此沒有關係。但如果在同一個作用域創建兩個同名的變量,那麼後一個變量會將前一個變量隱藏掉。
fn main() {
let mut name = "古明地覺";
println!("{}", name); // 古明地覺
// 這裏的 name 前面沒有 let
// 相當於變量的重新賦值,因此值的類型要和之前一樣
// 並且 name 必須可變
name = "古明地戀";
println!("{}", name); // "古明地戀"
let num = 123;
println!("{}", num); // 123
// 重新聲明 num,上一個 num 會被隱藏掉
// 並且兩個 num 沒有關係,是否可變、類型都可以自由指定
let mut num = 345u16;
println!("{}", num); // 345
}
變量的隱藏算是現代靜態語言中的一個比較獨特的特性了。
另外變量聲明的時候可以同時賦初始值,但將聲明和賦值分爲兩步也是可以的。
fn main() {
let name;
{
// 當前作用域沒有 name
// 那麼綁定的就是外層的 name
name = "古明地覺"
}
println!("{}", name); // 古明地覺
// 注意:光看 name = "古明地覺" 這行代碼的話
// 容易給人一種錯覺,認爲 name 是可變的
// 但其實不是的,我們只是將聲明和賦值分成了兩步而已
// 如果再賦一次值的話就會報錯了,因爲我們修改了一個不可變的變量
// name = "古明地戀"; // 不合法,因爲修改了不可變的變量
}
如果變量聲明之後沒有賦初始值,那麼該變量就是一個未初始化的變量。而 Rust 不允許使用未初始化的變量,因爲會產生未定義行爲。
原生類型的轉換
接下來是類型轉換,首先 Rust 不提供原生類型之間的隱式轉換,如果想轉換,那麼必須使用 as 關鍵字顯式轉換。
fn main() {
let pi = 3.14f32;
// 下面的語句是不合法的,因爲類型不同
// let int: u8 = pi
// Rust 不支持隱式轉換,但可以使用 as
let int: u8 = pi as u8;
// 轉換之後會被截斷
println!("{} {}", pi, int); // 3.14 3
// 整數也可以轉成 char 類型
let char = 97 as char;
println!("{}", char); // a
// 但是整數在轉化的時候要注意溢出的問題
// 以及無符號和有符號的問題
let num = -10;
// u8 無法容納負數,那麼轉成 u8 的結果就是
// 2 的 8 次方 + num
println!("{}", num as u8); // 246
let num = -300;
// -300 + 256 = -44,但 -44 還小於 0
// 那麼繼續加,-44 + 256 = 212
println!("{}", num as u8); // 212
// 轉成 u16 就是 2 的 16 次方 + num
println!("{}", num as u16); // 65526
// 以上有符號和無符號,然後是溢出的問題
let num = 300u16;
println!("{}", num as u8); // 44
// 轉成 u8 相當於只看最後 8 位
// 那麼 num as u8 就等價於
println!("{}", num & 0xFF); // 44
}
as 關鍵字只允許原生類型之間的轉換,如果你想把包含 4 個元素的 u8 數組轉成一個 u32 整數,那麼 as 就不允許了。儘管在邏輯上這是成立的,但 Rust 覺得不安全,如果你非要轉的話,那麼需要使用 Rust 提供的一種更高級的轉換,並且還要使用 unsafe。
fn main() {
// 轉成二進制的話就是
// arr[0] -> 00000001
// arr[1] -> 00000010
// arr[2] -> 00000011
// arr[3] -> 00000100
let arr: [u8; 4] = [1, 2, 3, 4];
// 4 個 u8 可以看成是一個 u32
// 由於 Rust 採用的是小端存儲
// 所以轉成整數就是
let num = 0b00000100_00000011_00000010_00000001;
println!("{}", num);
// 我們也可以使用 Rust 提供的更高級的類型轉換
unsafe {
println!("{}", std::mem::transmute::<[u8; 4], u32>(arr))
}
/*
67305985
67305985
*/
}
可以看到結果和我們想的是一樣的。然後關於 unsafe 這一塊暫時無需關注,包括裏面那行復雜的類型轉換暫時也不用管,我們會在後續解釋它們,目前只需要知道有這麼個東西即可。
自定義類型的轉換
看完了原生類型的轉換,再來看看自定義類型,也就是結構體和枚舉。針對於自定義類型的轉換,Rust 是基於 trait 實現的,在 Rust 裏面有一個叫 From 的 trait,它內部定義了一個 from 方法。
因此如果類型 T 實現 From trait,那麼通過 T::from 便可以基於其它類型的值生成自己。
#[derive(Debug)]
struct Number {
val: i32
}
// From 定義了一個泛型 T
// 因此在實現 From 的時候還要指定泛型的具體類型
impl From<i32> for Number {
// 在調用 Number::from(xxx) 的時候
// 就會自動執行這裏的 from 方法
// 因爲實現的是 From<i32>,那麼 xxx 也必須是 i32
// 再注意一下這裏的 Self,它表示的是當前的結構體類型
// 但顯然我們寫成 Number 也是可以的,不過更建議寫成 Self
fn from(item: i32) -> Self {
Number { val: item }
}
}
fn main() {
println!("{:?}", Number::from(666));
/*
Number { val: 666 }
*/
// 再比如 String::from,首先 String 也是個結構體
// 顯然它實現了 From<&str>
println!("{}", String::from("你好"));
/*
你好
*/
}
既然有 From,那麼就有 Into,Into 相當於是把 From 給倒過來了。並且當你實現了 From,那麼自動就獲得了 Into。
#[derive(Debug)]
struct Number {
val: u16
}
impl From<u16> for Number {
fn from(item: u16) -> Self {
Number { val: item }
}
}
fn main() {
println!("{:?}", Number::from(666));
/*
Number { val: 666 }
*/
// 由於不同的類型都可以實現 From<u16> trait
// 那麼在調用 666u16.into() 的時候,編譯器就不知道轉成哪種類型
// 因此這裏需要顯式地進行類型聲明
let n: Number = 666u16.into();
println!("{:?}", n); // Number { val: 666 }
}
另外裏面的 666u16 寫成 666 也是可以的。因爲調用了 into 方法,Rust 會根據上下文將其推斷爲 u16。
但如果我們指定了類型,並且類型不是 u16,比如 666u8,那麼就不行了。因爲 Number 沒有實現 From,它實現的是 From,除非我們再單獨實現一個 From。
然後除了 From 和 Into 之外,還有 TryFrom 和 TryInto,它們用於易出錯的類型轉換,返回值是 Result 類型。我們看一下 TryFrom 的定義:
trait TryFrom<T> {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
如果簡化一下,那麼就是這個樣子,我們需要實現 try_from 方法,並且要給某個類型起一個別名叫 Error。
// TryFrom 和 TryInto 需要先導入
use std::convert::TryFrom;
use std::convert::TryInto;
#[derive(Debug)]
struct IsAdult {
age: u8
}
impl TryFrom<u8> for IsAdult {
type Error = &'static str;
fn try_from(item: u8) -> Result<Self, Self::Error> {
if item >= 18 {
Ok(IsAdult{age: item})
} else {
Err("未成年")
}
}
}
fn main() {
let p1 = IsAdult::try_from(18);
let p2 = IsAdult::try_from(17);
println!("{:?}", p1);
println!("{:?}", p2);
/*
Ok(IsAdult { age: 18 })
Err("未成年")
*/
// 實現了 TryFrom 也自動實現了 TryInto
let p3: Result<IsAdult, &'static str> = 20.try_into();
let p4: Result<IsAdult, &'static str> = 15.try_into();
println!("{:?}", p3);
println!("{:?}", p4);
/*
Ok(IsAdult { age: 20 })
Err("未成年")
*/
}
最後再來介紹一個叫 ToString 的 trait,只要實現了這個 trait,那麼便可以調用 to_string 方法轉成字符串。因爲不管什麼類型的對象,我們都希望能將它打印出來。
use std::string::ToString;
struct IsAdult {
age: u8
}
// ToString 不帶泛型參數
// 只有一個 to_string 方法,我們實現它即可
impl ToString for IsAdult {
fn to_string(&self) -> String {
format!("age = {}", self.age)
}
}
fn main() {
let p = IsAdult{age: 18};
println!("{}", p.to_string());
/*
age = 18
*/
}
但很明顯,對於當前這個例子來說,即使我們不實現 trait、只是單純地實現一個方法也是可以的。
流程控制
任何一門編程語言都會包含流程控制,在 Rust 裏面有 if/else, for, while, loop 等等,讓我們來看一看它們的用法。
if / else
Rust 的 if / else 和其它語言類似,但 Rust 的布爾判斷條件不必使用小括號包裹,且每個條件後面都跟着一個代碼塊。並且 if / else 是一個表達式,所有分支都必須返回相同的類型。
fn degree(age: u8) -> String {
if age > 90 {
// &str 也實現了 ToString trait
"A".to_string()
} else if age > 80 {
"B".to_string()
} else if age > 60 {
"C".to_string()
} else {
"D".to_string()
}
// if 表達式的每一個分支都要返回相同的類型
// 然後執行的某個分支的返回值會作爲整個 if 表達式的值
}
fn main() {
println!("{}", degree(87));
println!("{}", degree(97));
println!("{}", degree(57));
/*
B
A
D
*/
}
Rust 沒有提供三元運算符,因爲在 Rust 裏面 if 是一個表達式,那麼它可以輕鬆地實現三元運算符。
fn main() {
let number = 107;
let normailize = if number > 100 {100}
else if number < 0 {0} else {number};
println!("{}", normailize); // 100
}
以上就是 Rust 的 if / else。
loop 循環
Rust 提供了 loop,不需要條件,表示無限循環。想要跳出的話,需要在循環內部使用 break。
fn main() {
let mut count = 0;
loop {
count += 1;
if count == 3 {
// countinue 後面加不加分號均可
continue;
}
println!("count = {}", count);
if count == 5 {
println!("ok, that's enough");
break;
}
}
/*
count = 1
count = 2
count = 4
count = 5
ok, that's enough
*/
}
最後 loop 循環有一個比較強大的功能,就是在使用 break 跳出循環的時候,break 後面的值會作爲整個 loop 循環的返回值。
fn main() {
let mut count = 0;
let result = loop {
count += 1;
if count == 3 {
continue;
}
if count == 5 {
break 1234567;
}
};
println!("result = {}", result);
/*
result = 1234567
*/
}
這個特性還是很有意思的。
然後 loop 循環還支持打標籤,可以更方便地跳出循環。
fn main() {
let mut count = 0;
// break 和 continue 針對的都是當前所在的循環
// 加上標籤的話,即可作用指定的循環
let word = 'outer: loop {
println!("進入外層循環");
if count == 1 {
// 這裏的 break 等價於 break 'outer
println!("跳出外層循環");
break "嘿嘿,結束了";
}
'inner: loop {
println!("進入內層循環");
count += 1;
// 這裏如果只寫 continue
// 那麼等價於 continue 'inner
continue 'outer;
};
};
/*
進入外層循環
進入內層循環
進入外層循環
跳出外層循環
*/
println!("{}", word);
/*
嘿嘿,結束了
*/
}
注意一下標籤,和生命週期一樣,必須以一個單引號開頭。
for 循環
while 循環和其它語言類似,這裏不贅述了,直接來看 for 循環。for 循環遍歷的一般都是迭代器,而創建迭代器最簡單的辦法就是使用區間標記,比如 a..b,會生成從 a 到 b(不包含 b)、步長爲 1 的一系列值。
fn main() {
let mut sum = 0;
for i in 1..101 {
sum += i;
}
println!("{}", sum); // 5050
sum = 0;
// 如果是 ..=,那麼表示包含結尾
for i in 1..=100 {
sum += i;
}
println!("{}", sum); // 5050
}
然後再來說一說迭代器,for 循環在遍歷集合的時候,會自動調用集合的某個方法,將其轉換爲迭代器,然後再遍歷,這一點和 Python 是比較相似的。那麼都有哪些方法,調用之後可以得到集合的迭代器呢?
首先是 iter 方法,在遍歷的時候會得到元素的引用,這樣集合在遍歷結束之後仍可以使用。
fn main() {
let names = vec![
"satori".to_string(),
"koishi".to_string(),
"marisa".to_string(),
];
// names 是分配在堆上的,如果遍歷的是 names
// 那麼遍歷結束之後 names 就不能再用了
// 因爲在遍歷的時候,所有權就已經發生轉移了
// 所以我們需要遍歷 names.iter()
// 因爲 names.iter() 獲取的是 names 的引用
// 而在遍歷的時候,拿到的也是每個元素的引用
for name in names.iter() {
println!("{}", name);
}
/*
satori
koishi
marisa
*/
println!("{:?}", names);
/*
["satori", "koishi", "marisa"]
*/
}
循環結束之後,依舊可以使用 names。
然後是 into_iter 方法,此方法會轉移所有權,它和遍歷 names 是等價的。
我們看到在遍歷 names 的時候,會隱式地調用 names.into_iter()。如果後續不再使用 names,那麼可以調用此方法,讓 names 將自身的所有權交出去。當然啦,我們也可以直接遍歷 names,兩者是等價的。
最後是 iter_mut 方法,它和 iter 是類似的,只不過拿到的是可變引用。
fn main() {
let mut numbers = vec![1, 2, 3];
// numbers.iter() 獲取的是 numbers 的引用(不可變引用)
// 然後遍歷得到的也是每個元素的引用(同樣是不可變引用)
// numbers.iter_mut() 獲取的是 numbers 的可變引用
// 然後遍歷得到的也是每個元素的可變引用
// 既然拿到的是可變引用,那麼 numbers 必須要聲明爲 mut
for number in numbers.iter_mut() {
// 這裏的 number 就是 &mut i32
// 修改引用指向的值
*number *= 2;
}
// 可以看到 numbers 變了
println!("{:?}", numbers); // [2, 4, 6]
}
以上就是創建迭代器的幾種方式,最後再補充一點,迭代器還可以調用一個 enumerate 方法,能夠將索引也一塊返回。
fn main() {
let mut names = vec![
"satori".to_string(),
"koishi".to_string(),
"marisa".to_string(),
];
for (index, name) in names.iter_mut().enumerate() {
name.push_str(&format!(", 我是索引 {}", index));
}
println!("{:#?}", names);
/*
[
"satori, 我是索引 0",
"koishi, 我是索引 1",
"marisa, 我是索引 2",
]
*/
}
調用 enumerate 方法之後,會將遍歷出來的值封裝成一個元組,其中第一個元素是索引。
match 匹配
Rust 通過 match 關鍵字來提供模式匹配,和 C 語言的 switch 用法類似。會執行第一個匹配上的分支,並且所有可能的值必須都要覆蓋。
fn main() {
let number = 20;
match number {
// 匹配單個值
1 => println!("number = 1"),
// 匹配多個值
2 | 5 | 6 | 7 | 10 => {
println!("number in [2, 5, 6, 7, 10]")
},
// 匹配一個區間範圍
11..=19 => println!("11 <= number <= 19"),
// match 要求分支必須覆蓋所有可能出現的情況
// 但明顯數字是無窮的,於是我們可以使用下劃線代表默認分支
_ => println!("other number")
}
/*
other number
*/
let flag = true;
match flag {
true => println!("flag is true"),
false => println!("flag is false"),
// true 和 false 已經包含了所有可能出現的情況
// 因此下面的默認分支是多餘的,但可以有
_ => println!("unreachable")
}
/*
flag is true
*/
}
對於數值和布爾值,我們更多用的是 if。然後 match 也可以處理更加複雜的結構,比如元組:
fn main() {
let t = (1, 2, 3);
match t {
(0, y, z) => {
println!("第一個元素爲 0,第二個元素爲 {}\
,第三個元素爲 {}", y, z);
},
// 使用 .. 可以忽略部分選項,但 .. 只能出現一次
// (x, ..) 只關心第一個元素
// (.., x) 只關心最後一個元素
// (x, .., y) 只關心第一個和最後一個元素
// (x, .., y, z) 只關心第一個和最後兩個元素
// (..) 所有元素都不關心,此時效果等價於默認分支
(1, ..) => {
println!("第一個元素爲 1,其它元素不關心")
},
(..) => {
println!("所有元素都不關心")
},
_ => {
// 由於 (..) 分支的存在,默認分支永遠不可能執行
println!("默認分支")
}
}
/*
第一個元素爲 1,其它元素不關心
*/
}
然後是枚舉:
fn main() {
enum Color {
RGB(u32, u32, u32),
HSV(u32, u32, u32),
HSL(u32, u32, u32),
}
let color = Color::RGB(122, 45, 203);
match color {
Color::RGB(r, g, b) => {
println!("r = {}, g = {}, b = {}",
r, g, b);
},
Color::HSV(h, s, v) => {
println!("h = {}, s = {}, v = {}",
h, s, v);
},
Color::HSL(h, s, l) => {
println!("h = {}, s = {}, l = {}",
h, s, l);
}
}
/*
r = 122, g = 45, b = 203
*/
}
接下來是結構體:
fn main() {
struct Point {
x: (u32, u32),
y: u32
}
let p = Point{x: (1, 2), y: 5};
// 之前說過,可以使用下面這種方式解構
// let Point { x, y } = p
// 對於使用 match 來說,也是如此
match p {
Point { x, y } => {
println!("p.x = {:?}, p.y = {}", x, y);
}
// 如果不關心某些成員的話,那麼也可以使用 ..
// 比如 Point {x, ..},表示你不關心 y
}
/*
p.x = (1, 2), p.y = 5
*/
}
最後來看一下,如何對引用進行解構。首先要注意的是:解引用和解構是兩個完全不同的概念。解引用使用的是 *,解構使用的是 &。
fn main() {
let mut num = 123;
// 獲取一個 i32 的引用
let refer = &mut num;
// refer 是一個引用,可以通過 *refer 解引用
// 並且在打印的時候,refer 和 *refer 是等價的
println!("refer = {}, *refer = {}", refer, *refer);
/*
refer = 123, *refer = 123
*/
// 也可以修改引用指向的值
// refer 引用的是 num,那麼要想修改的話
// num 必須可變,refer 也必須是 num 的可變引用
*refer = 1234;
println!("num = {}", num);
/*
num = 1234
*/
// 字符串也是同理
let mut name = "komeiji".to_string();
let refer = &mut name;
// 修改字符串,將首字母大寫
*refer = "Komeiji".to_string();
println!("{}", name); // Komeiji
}
以上便是解引用,再來看看引用的解構。
fn main() {
let num = 123;
let refer = #
match refer {
// 如果用 &val 這個模式去匹配 refer
// 相當於做了這樣的比較,因爲 refer 是 &i32
// 而模式是 &val,那麼相當於將 refer 引用的值拷貝給了 val
&val => {
println!("refer 引用的值 = {}", val)
} // 如果 refer 是可變引用,那麼這裏的模式就應該是 &mut val
};
/*
refer 引用的值 = 123
*/
// 如果不想使用 &,那麼就要在匹配的時候解引用
match *refer {
val => {
println!("refer 引用的值 = {}", val)
}
};
/*
refer 引用的值 = 123
*/
}
最後我們創建引用的時候,除了可以使用 & 之外,還可以使用 ref 關鍵字。
fn main() {
let num = 123;
// let refer = # 可以寫成如下
let ref refer = num;
println!("{} {} {}", refer, *refer, num);
/*
123 123 123
*/
// 引用和具體的值在打印上是沒有區別的
// 但從結構上來說,兩者卻有很大區別
// 比如我們可以對 refer 解引用,但不能對 num 解引用
// 創建可變引用
let mut num = 345;
{
let ref mut refer = num;
*refer = *refer + 1;
println!("{} {}", refer, *refer);
/*
346 346
*/
}
println!("{}", num); // 346
// 然後模式匹配也可以使用 ref
let num = 567;
match num {
// 此時我們應該把 ref refer 看成是一個整體
// 所以 ref refer 整體是一個 i32
// 那麼 refer 是啥呢?顯然是 &i32
ref refer => println!("{} {}", refer, *refer),
}
/*
567 567
*/
let mut num = 678;
match num {
// 顯然 refer 就是 &mut i32
ref mut refer => {
*refer = 789;
}
}
println!("{}", num); // 789
}
以上就是 match 匹配,但是在引用這一塊,需要多體會一下。
另外在使用 match 的時候,還可以搭配衛語句,用於過濾分支,舉個例子:
fn match_tuple(t: (i32, i32)) {
match t {
// (x, y) 已經包含了所有的情況
// 但我們又給它加了一個限制條件
// 就是兩個元素必須相等
(x, y) if x == y => {
println!("t[0] == t[1]")
},
(x, y) if x > y => {
println!("t[0] > t[1]")
},
// 此時就不需要衛語句了,該分支的 x 一定小於 y
// 並且這裏加上衛語句反而會報錯,因爲加上之後
// Rust 無法判斷分支是否覆蓋了所有的情況
// 所以必須有 (x, y) 或者默認分支進行兜底
(x, y) => {
println!("t[0] < t[1]")
},
}
}
fn main() {
match_tuple((1, 2));
match_tuple((1, 1));
match_tuple((3, 1));
/*
t[0] < t[1]
t[0] == t[1]
t[0] > t[1]
*/
}
總的來說,衛語句用不用都是可以的,我們完全可以寫成 (x, y),匹配上之後在分支裏面做判斷。
最後 match 還有一個綁定的概念,看個例子:
fn main() {
let num = 520;
match num {
// 該分支一定可以匹配上
// 匹配之後會將 num 賦值給 n
n => {
if n == 520 {
println!("{} 代表 ❥(^_-)", n)
} else {
println!("意義不明的數字")
}
}
}
/*
520 代表 ❥(^_-)
*/
// 我們可以將 520 這個分支單獨拿出來
match num {
// 匹配完之後,會自動將 520 綁定在 n 上面
n @ 520 => println!("{} 代表 ❥(^_-)", n),
n => println!("意義不明的數字")
}
/*
520 代表 ❥(^_-)
*/
// 當然啦,我們還可以使用衛語句
match num {
n if n == 520 => println!("{} 代表 ❥(^_-)", n),
n => println!("意義不明的數字")
}
/*
520 代表 ❥(^_-)
*/
}
這幾個功能彼此之間都是很相似的,用哪個都可以。
if let
在一些簡單的場景下,使用 match 其實並不優雅,舉個例子。
fn main() {
let num = Some(777);
match num {
Some(n) => println!("{}", n),
// 因爲 match 要覆蓋所有情況,所以這一行必須要有
// 但如果我們不關心默認情況的話,那麼就有點多餘了
_ => ()
}
/*
777
*/
// 所以當我們只關心一種情況,其它情況忽略的話
// 那麼使用 if let 會更加簡潔
if let Some(i) = num {
println!("{}", i);
}
/*
777
*/
// 當然 if let 也支持 else if let 和 else
let score = 78;
if let x @ 90..=100 = score {
println!("你的分數 {} 屬於 A 級", x)
} else if let x @ 80..=89 = score {
println!("你的分數 {} 屬於 B 級", x)
} else if let 60..=79 = score {
println!("你的分數 {} 屬於 C 級", score)
}
/*
你的分數 78 屬於 C 級
*/
// 顯然對於當前這種情況就不適合用 if let 了
// 此時應該使用 match 或者普通的 if 語句
// 總之:match 一般用來處理枚舉
// 如果不是枚舉,那麼用普通的 if else 就好
// 如果只關注枚舉的一種情況,那麼使用 if let
}
注意:if let 也可以搭配 else if 語句。
小結
以上我們就回顧了一下 Rust 的基礎知識,包括原生類型、自定義類型、變量綁定、類型系統、類型轉換、流程控制。下一篇文章我們來回顧 Rust 的函數和泛型。
Reference:
- https://rustwiki.org/zh-CN/rust-by-example/index.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8Hyk9UMzUU2TN9_4wyNJxg