標準函式庫之旅
現在你已經知道了很多 Rust 的知識,你將能夠理解標準函式庫裡面大多數的東西。它裡面的程式碼已經不再那麼可怕了。讓我們來看看它裡面一些我們還沒有學過的部分。這次旅程將介紹標準函式庫裡不需要安裝 Rust 的絕大多數部分。我們將重溫很多我們已經知道的內容,這樣我們就可以更深入地學習它們。
陣列
在過去的版本 (Rust 1.53 以前) 裡陣列 (Array) 還沒有實作 Iterator
,你要在 for
迴圈裡對它們使用像是 .iter()
的方法。(人們以前也常在 for
迴圈裡用 &
來得到切片。)因此這個範例在過去是不能執行的:
fn main() { let my_cities = ["Beirut", "Tel Aviv", "Nicosia"]; for city in my_cities { println!("{}", city); } }
編譯器常常給出這段訊息:
error[E0277]: `[&str; 3]` is not an iterator
--> src\main.rs:5:17
|
| ^^^^^^^^^ borrow the array with `&` or call `.iter()` on it to iterate over it
幸運的是那不再是問題了!所以這三種用法全都能用:
fn main() { let my_cities = ["Beirut", "Tel Aviv", "Nicosia"]; for city in my_cities { println!("{}", city); } for city in &my_cities { println!("{}", city); } for city in my_cities.iter() { println!("{}", city); } }
印出:
Beirut
Tel Aviv
Nicosia
Beirut
Tel Aviv
Nicosia
Beirut
Tel Aviv
Nicosia
如果你想從陣列中得到變數,你可以把它們的變數名放在 []
中來解構它。這和在 match
陳敘式中使用元組或從結構體中得到變數是一樣的。
fn main() { let my_cities = ["Beirut", "Tel Aviv", "Nicosia"]; let [city1, city2, city3] = my_cities; println!("{}", city1); }
印出 Beirut
。
字元
你可以使用 .escape_unicode()
方法來得到字元 (char) 的 Unicode 號碼。
fn main() { let korean_word = "청춘예찬"; for character in korean_word.chars() { print!("{} ", character.escape_unicode()); } }
印出 \u{ccad} \u{cd98} \u{c608} \u{cc2c}
。
你可以使用 From
特徵從 u8
中得到字元,但是從 u32
時,你要使用 TryFrom
,因為它有可能不成功。u32
可容納的數字比 Unicode 中的字元多很多。我們可以透過簡單的示範來觀察到這件事。
use std::convert::TryFrom; // 你需要引進 TryFrom 來使用它 use rand::prelude::*; // 我們也將會用到隨機數 fn main() { let some_character = char::from(99); // 這個容易 - 不需要 TryFrom println!("{}", some_character); let mut random_generator = rand::thread_rng(); // 這將會嘗試 40,000 次來從 u32 做出字元. // 範圍從 0 (std::u32::MIN) 到 u32 的最大數值 (std::u32::MAX). 如果它不成功, 我們會給它 '-'. for _ in 0..40_000 { let bigger_character = char::try_from(random_generator.gen_range(std::u32::MIN..std::u32::MAX)).unwrap_or('-'); print!("{}", bigger_character) } }
幾乎每次都會產生 -
。這是你會看到的那種輸出的一部分:
------------------------------------------------------------------------𤒰---------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-------------------------------------------------------------춗--------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
----------------------------------------------------------------
所以你需要使用 TryFrom
其實是件好事。
另外,從 2020 年八月底開始,你現在可以從 char
中得到 String
。(String
實作了 From<char>
) 只要寫 String::from()
,然後把 char
放在裡面。
整數
給這些整數型別用的數學方法有很多,再加上一些其他用途的方法。這裡是一些最有用的:
.checked_add()
、.checked_sub()
、.checked_mul()
、.checked_div()
。如果你認為你可能會得到一個不適合型別的數字,這些都是不錯的方法。它們會回傳 Option
,這樣你就可以安全地檢查你的數學運算是否正常,而不會讓程式恐慌。
fn main() { let some_number = 200_u8; let other_number = 200_u8; println!("{:?}", some_number.checked_add(other_number)); println!("{:?}", some_number.checked_add(1)); }
印出:
None
Some(201)
你會注意到,在整數的頁面上經常說著 rhs
。這意味著"右手邊(right hand side)",也就是你做一些數學運算時右手邊的運算元。比如在 5 + 6
中,5
在左、6
在右,所以 6
就是 rhs
。這個不是關鍵詞,但是你會經常看到,所以先知道比較好。
說到這裡,我們來學習一下如何實作 Add
。在你實作了 Add
之後,你可以在你建立的型別上使用 +
。你需要自己實作 Add
,因為 add 可以表達很多意思。這是標準函式庫頁面中的範例:
#![allow(unused)] fn main() { use std::ops::Add; // 首先引進 Add #[derive(Debug, Copy, Clone, PartialEq)] // PartialEq 大概是這裡最重要的部份了. 你會想要讓數字能比較 struct Point { x: i32, y: i32, } impl Add for Point { type Output = Self; // 記得嗎, 這叫做"關聯型別": "一起出現的型別". // 這個情況下這不過是另一個 Point fn add(self, other: Self) -> Self { Self { x: self.x + other.x, y: self.y + other.y, } } } }
現在讓我們為自己的型別實作 Add
。讓我們想像我們想把兩個國家加在一起,這樣我們就可以比較它們的經濟。那看起來像這樣:
use std::fmt; use std::ops::Add; #[derive(Clone)] struct Country { name: String, population: u32, gdp: u32, // 這是經濟大小 } impl Country { fn new(name: &str, population: u32, gdp: u32) -> Self { Self { name: name.to_string(), population, gdp, } } } impl Add for Country { type Output = Self; fn add(self, other: Self) -> Self { Self { name: format!("{} and {}", self.name, other.name), // 我們會一起加上名稱, population: self.population + other.population, // 以及人口數, gdp: self.gdp + other.gdp, // 和 GDP } } } impl fmt::Display for Country { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "In {} are {} people and a GDP of ${}", // 然後我們可以只用 {} 把它們全部印出來 self.name, self.population, self.gdp ) } } fn main() { let nauru = Country::new("Nauru", 10_670, 160_000_000); let vanuatu = Country::new("Vanuatu", 307_815, 820_000_000); let micronesia = Country::new("Micronesia", 104_468, 367_000_000); // 我們可以給予 Country 的 name 是個 &str 而不是 String. 但是我們就需要到處寫上生命週期 // 並且那樣對小範例來說就太多東西了. 比較好的方式是只在我們呼叫 println! 時克隆它們. println!("{}", nauru.clone()); println!("{}", nauru.clone() + vanuatu.clone()); println!("{}", nauru + vanuatu + micronesia); }
印出:
In Nauru are 10670 people and a GDP of $160000000
In Nauru and Vanuatu are 318485 people and a GDP of $980000000
In Nauru and Vanuatu and Micronesia are 422953 people and a GDP of $1347000000
以後在這段程式碼中,我們可以把 .fmt()
改為顯示更容易閱讀的數字。
另外三個叫 Sub
、Mul
和 Div
,實作起來基本一樣。+=
、-=
、*=
和 /=
,只要加上 Assign
:AddAssign
、SubAssign
、MulAssign
和 DivAssign
即可。你可以在這裡看到完整的列表,因為還有很多。例如 %
被稱為 Rem
,-
被稱為 Neg
,等等。
浮點數
f32
和 f64
有非常大量的方法讓你在做數學運算時使用。我們不會去看這些東西,但這裡有一些你可能會用到的方法。它們分別是 .floor()
、.ceil()
、.round()
和 .trunc()
。所有這些方法都會回傳像整數的 f32
或者 f64
,但小數點後面是 0
。它們是這樣做的:
.floor()
:給你下一個最低的整數。.ceil()
:給你下一個最高的整數。.round()
:給你較大的整數,如果小數大於等於 0.5;或是相同的整數,如果小數小於 0.5。這就是所謂的四捨五入,因為它給你"捨去或進位(round)"的數字(數字的精簡形式)。.trunc()
:只是切除掉小數點號後的部分。截斷(Truncate)是"切除"的意思。
這裡是個簡單的函式來印出它們。
fn four_operations(input: f64) { println!( "For the number {}: floor: {} ceiling: {} rounded: {} truncated: {}\n", input, input.floor(), input.ceil(), input.round(), input.trunc() ); } fn main() { four_operations(9.1); four_operations(100.7); four_operations(-1.1); four_operations(-19.9); }
印出:
For the number 9.1:
floor: 9
ceiling: 10
rounded: 9 // because less than 9.5
truncated: 9
For the number 100.7:
floor: 100
ceiling: 101
rounded: 101 // because more than 100.5
truncated: 100
For the number -1.1:
floor: -2
ceiling: -1
rounded: -1
truncated: -1
For the number -19.9:
floor: -20
ceiling: -19
rounded: -20
truncated: -19
f32
和 f64
有方法叫做 .max()
和 .min()
,可以得到兩個數字中較大或較小的數字。(對於其他型別,你可以直接使用 std::cmp::max
和 std::cmp::min
。)這裡的範例是用 .fold()
來得到最高或最低數字的方法。你可以再次看到 .fold()
不僅僅是用來加數字的。
fn main() { let my_vec = vec![8.0_f64, 7.6, 9.4, 10.0, 22.0, 77.345, 10.22, 3.2, -7.77, -10.0]; let maximum = my_vec.iter().fold(f64::MIN, |current_number, next_number| current_number.max(*next_number)); // 註: 從 f64 的最低可能的數字開始. let minimum = my_vec.iter().fold(f64::MAX, |current_number, next_number| current_number.min(*next_number)); // 而這裡則從最高可能的數字開始 println!("{}, {}", maximum, minimum); }
布林
在 Rust 中,如果你願意,你可以把 bool
變成整數,因為這樣做是安全的。但你不能反過來做。如你所見,true
變成了 1,false
變成了 0。
fn main() { let true_false = (true, false); println!("{} {}", true_false.0 as u8, true_false.1 as i32); }
印出 1 0
。或者是如果你告訴編譯器型別,也可以使用 .into()
:
fn main() { let true_false: (i128, u16) = (true.into(), false.into()); println!("{} {}", true_false.0, true_false.1); }
印出的是一樣的東西。
從 Rust 1.50 (2021 年 2 月釋出)開始,有個叫做 then()
的方法,它將 bool
變成 Option
。使用 then()
時需要接受閉包,如果元素是true
,閉包就會被呼叫。另外,無論從閉包中回傳什麼,都會放入 Option
裡。這裡是個小範例:
fn main() { let (tru, fals) = (true.then(|| 8), false.then(|| 8)); println!("{:?}, {:?}", tru, fals); }
只是印出 Some(8), None
。
而現在是個長一點的範例:
fn main() { let bool_vec = vec![true, false, true, false, false]; let option_vec = bool_vec .iter() .map(|item| { item.then(|| { // 把這個放在 map 裡面那我們才可以把它傳下去 println!("Got a {}!", item); "It's true, you know" // 如果是 true 就把這個放進 Some 裡 // 不然就只傳 None 下去 }) }) .collect::<Vec<_>>(); println!("Now we have: {:?}", option_vec); // 那也會印出 Nones. 讓我們從 map 過濾它們到新的向量裡. let filtered_vec = option_vec.into_iter().filter_map(|c| c).collect::<Vec<_>>(); println!("And without the Nones: {:?}", filtered_vec); }
這裡是印出的內容:
Got a true!
Got a true!
Now we have: [Some("It\'s true, you know"), None, Some("It\'s true, you know"), None, None]
And without the Nones: ["It\'s true, you know", "It\'s true, you know"]
向量
Vec(向量)有很多方法我們還沒有看過。先來說說 .sort()
。.sort()
一點都不意外,使用了 &mut self
來對向量進行排序。
fn main() { let mut my_vec = vec![100, 90, 80, 0, 0, 0, 0, 0]; my_vec.sort(); println!("{:?}", my_vec); }
印出 [0, 0, 0, 0, 0, 80, 90, 100]
。但還有一種更有趣的排序方式叫 .sort_unstable()
,它通常更快。它之所以更快,是因為它不在乎排序前後相同數字的先後順序。在常規的 .sort()
中,你知道最後的 0, 0, 0, 0, 0
會在 .sort()
之後的順序相同。但是 .sort_unstable()
可能會把最後一個零移到索引 0,然後把倒數第三個零移到索引 2,等等。
.dedup()
的意思是"去重複"(de-duplicate)。它將刪除向量中相同的元素,但只有當它們彼此相鄰時才會刪除。接下來這段程式碼不會只印出 "sun", "moon"
。
fn main() { let mut my_vec = vec!["sun", "sun", "moon", "moon", "sun", "moon", "moon"]; my_vec.dedup(); println!("{:?}", my_vec); }
它只是把 "sun" 旁邊的另一個 "sun" 去掉,然後把 "moon" 旁邊的下一個 "moon" 去掉,再把 "moon" 旁邊的另一個 "moon" 去掉。結果是 ["sun", "moon", "sun", "moon"]
。
如果你想把每個重複的都去掉,就先 .sort()
:
fn main() { let mut my_vec = vec!["sun", "sun", "moon", "moon", "sun", "moon", "moon"]; my_vec.sort(); my_vec.dedup(); println!("{:?}", my_vec); }
結果:["moon", "sun"]
。
字串
你會記得 String
有點像是一種 Vec
。它很像 Vec
讓你可以呼叫很多相同的方法。比如說,你可以用 String::with_capacity()
建立字串,尤其是如果你會需要一直用 .push()
推進 char
多次,或者用 .push_str()
推進 &str
。這裡是個對 String
有太多次記憶體分配 (allocation) 的範例。
fn main() { let mut push_string = String::new(); let mut capacity_counter = 0; // 容量從 0 開始 for _ in 0..100_000 { // 做 100,000 次 if push_string.capacity() != capacity_counter { // 首先檢查容量現在是否不同 println!("{}", push_string.capacity()); // 如果是就印出來 capacity_counter = push_string.capacity(); // 再來更新計數器 } push_string.push_str("I'm getting pushed into the string!"); // 並且每次推這個字串進去 } }
印出:
35
70
140
280
560
1120
2240
4480
8960
17920
35840
71680
143360
286720
573440
1146880
2293760
4587520
我們不得不重分配(reallocate,把所有東西複製過來到另一處記憶體位置) 18次。但既然我們知道了最終的容量(capacity),那麼我們將馬上給它容量,就不需要重分配:只要一個 String
容量值就夠了。
fn main() { let mut push_string = String::with_capacity(4587520); // 我們知道明確的數字. 一些不同的大數字也行得通 let mut capacity_counter = 0; for _ in 0..100_000 { if push_string.capacity() != capacity_counter { println!("{}", push_string.capacity()); capacity_counter = push_string.capacity(); } push_string.push_str("I'm getting pushed into the string!"); } }
印出 4587520
。完美!我們永遠不再需要分配了。
當然實際長度肯定比這個小。如果你試了 100001 次、101000 次等等,還是會說 4587520
。這是因為每次的容量都是之前的兩倍。不過我們可以用 .shrink_to_fit()
來縮小它(和 Vec
一樣)。我們的 String
已經非常大了,我們不想再給它增加任何東西,所以我們可以把它縮小一點。但是只有在你有把握的情況下才可以這樣做。這裡是原因:
fn main() { let mut push_string = String::with_capacity(4587520); let mut capacity_counter = 0; for _ in 0..100_000 { if push_string.capacity() != capacity_counter { println!("{}", push_string.capacity()); capacity_counter = push_string.capacity(); } push_string.push_str("I'm getting pushed into the string!"); } push_string.shrink_to_fit(); println!("{}", push_string.capacity()); push_string.push('a'); println!("{}", push_string.capacity()); push_string.shrink_to_fit(); println!("{}", push_string.capacity()); }
印出:
4587520
3500000
7000000
3500001
所以首先我們的大小是 4587520
,但我們沒有全部使用到。我們用了 .shrink_to_fit()
,然後把大小降到了 3500000
。但是我們忘記了需要推上 a
。當我們這樣做的時候,Rust 看到我們需要更多的空間,並加倍給了我們:現在是 7000000
。哎呀!所以我們又呼叫了 .shrink_to_fit()
一次,現在又回到了 3500001
。
.pop()
能用在 String
,就像用在 Vec
一樣。
fn main() { let mut my_string = String::from(".daer ot drah tib elttil a si gnirts sihT"); loop { let pop_result = my_string.pop(); match pop_result { Some(character) => print!("{}", character), None => break, } } }
印出 This string is a little bit hard to read.
因為它從最後一個字元開始。
.retain()
是使用閉包的方法,這對 String
來說很少見。就像在疊代器上的 .filter()
一樣。
fn main() { let mut my_string = String::from("Age: 20 Height: 194 Weight: 80"); my_string.retain(|character| character.is_alphabetic() || character == ' '); // 如果是字母或空白就保留 dbg!(my_string); // 為了好玩這次讓我們用 dbg!() 而不是 println! }
印出:
[src\main.rs:4] my_string = "Age Height Weight "
OsString 和 CString
std::ffi
是 std
的一部分,它幫助你將 Rust 與其他程式設計語言或作業系統一起使用。它有 OsString
和 CString
這樣的型別,它們就像給作業系統用的 String
或給 C 語言用的 String
一樣,它們各自也有自己的 &str
型別:OsStr
和 CStr
。ffi
的意思是"外部函式介面"(foreign function interface)。
當你必須與沒有 Unicode 的作業系統互動時,你可以使用 OsString
。Rust 所有的字串都是 unicode,但不是每個作業系統支援。這些是標準函式庫中關於為什麼我們會有 OsString
的簡單解釋。
- Unix (Linux 等等)上的字串可能是很多沒有零的位元組組合在一起。而且有時你會把它們讀取為 Unicode UTF-8。
- Windows 上的字串可能是由隨機的沒有零的 16 位元值組成。有時你會把它們讀取為 Unicode UTF-16。
- 在 Rust 中,字串總是有效的 UTF-8,其中可能包含多個零。
所以 OsString
被設計為可以被它們全部讀取到。
你可以用 OsString
來做所有常規的事情,比如 OsString::from("Write something here")
。它還有個有趣的方法叫做 .into_string()
,那會試圖把自己變成常規的 String
。它會回傳 Result
,但 Err
部分只是原來的 OsString
:
#![allow(unused)] fn main() { // 🚧 pub fn into_string(self) -> Result<String, OsString> }
所以如果不行用的話,那你就把它拿回來。你不能呼叫 .unwrap()
,因為它會恐慌,但是你可以使用 match
來拿回 OsString
。讓我們透過呼叫不存在的方法來測試一下:
use std::ffi::OsString; fn main() { // ⚠️ let os_string = OsString::from("This string works for your OS too."); match os_string.into_string() { Ok(valid) => valid.thth(), // 編譯器: "什麼是 .thth()??" Err(not_valid) => not_valid.occg(), // 編譯器: "什麼是 .occg()??" } }
然後編譯器準確地告訴我們什麼是我們想知道的:
error[E0599]: no method named `thth` found for struct `std::string::String` in the current scope
--> src/main.rs:6:28
|
6 | Ok(valid) => valid.thth(),
| ^^^^ method not found in `std::string::String`
error[E0599]: no method named `occg` found for struct `std::ffi::OsString` in the current scope
--> src/main.rs:7:37
|
7 | Err(not_valid) => not_valid.occg(),
| ^^^^ method not found in `std::ffi::OsString`
我們可以看到 valid
的型別是 String
以及 not_valid
的型別是 OsString
。
mem
std::mem
有一些非常有趣的方法。我們已經看到過一些了,比如 .size_of()
、.size_of_val()
和 .drop()
:
use std::mem; fn main() { println!("{}", mem::size_of::<i32>()); let my_array = [8; 50]; println!("{}", mem::size_of_val(&my_array)); let mut some_string = String::from("You can drop a String because it's on the heap"); mem::drop(some_string); // some_string.clear(); 如果我們這樣做就會恐慌 }
印出:
4
200
這裡是 mem
中的一些其他方法:
swap()
:用這個方法你可以交換兩個變數之間的值。你為每個變數建立可變參考來做到這件事。在你有兩樣東西想交換,卻因為借用規則 Rust 不允許時很有用。或是當你只想快速切換兩樣東西的時候。
這裡是一個範例:
use std::{mem, fmt}; struct Ring { // 從 Lord of the Rings 建立戒指 owner: String, former_owner: String, seeker: String, // 意思是 "尋求它的人" } impl Ring { fn new(owner: &str, former_owner: &str, seeker: &str) -> Self { Self { owner: owner.to_string(), former_owner: former_owner.to_string(), seeker: seeker.to_string(), } } } impl fmt::Display for Ring { // Display 用來秀出誰擁有它及誰想得到它 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} has the ring, {} used to have it, and {} wants it", self.owner, self.former_owner, self.seeker) } } fn main() { let mut one_ring = Ring::new("Frodo", "Gollum", "Sauron"); println!("{}", one_ring); mem::swap(&mut one_ring.owner, &mut one_ring.former_owner); // Gollum 拿回了戒指一下子 println!("{}", one_ring); }
會印出:
Frodo has the ring, Gollum used to have it, and Sauron wants it
Gollum has the ring, Frodo used to have it, and Sauron wants it
replace()
:這像是 swap,其實裡面也用了 swap,如同你看到的:
#![allow(unused)] fn main() { pub fn replace<T>(dest: &mut T, mut src: T) -> T { swap(dest, &mut src); src } }
所以它只是做交換,然後回傳另外一個。有了這個,你就能用放進去的其他東西來替換值。因為它會回傳舊的值,所以你應該用 let
來取得它。這裡是個便捷的範例:
use std::mem; struct City { name: String, } impl City { fn change_name(&mut self, name: &str) { let old_name = mem::replace(&mut self.name, name.to_string()); println!( "The city once called {} is now called {}.", old_name, self.name ); } } fn main() { let mut capital_city = City { name: "Constantinople".to_string(), }; capital_city.change_name("Istanbul"); }
印出 The city once called Constantinople is now called Istanbul.
。
有個函式叫 .take()
,和 .replace()
類似,但它在元素中留下了預設值。你會記得,預設值通常像是 0、"" 之類的東西。這裡是它的簽名:
#![allow(unused)] fn main() { // 🚧 pub fn take<T>(dest: &mut T) -> T where T: Default, }
所以你可以做像這樣的事情:
use std::mem; fn main() { let mut number_vec = vec![8, 7, 0, 2, 49, 9999]; let mut new_vec = vec![]; number_vec.iter_mut().for_each(|number| { let taker = mem::take(number); new_vec.push(taker); }); println!("{:?}\n{:?}", number_vec, new_vec); }
如同你看到的,所有數字都被替換為 0:沒有任何索引的元素被刪除。
[0, 0, 0, 0, 0, 0]
[8, 7, 0, 2, 49, 9999]
對於你自己的型別,你當然可以把 Default
實現成任何你想要的型別。讓我們來看看我們的 Bank
和 Robber
的範例。每次他搶了 Bank
,他就會在桌子上拿到錢。但是辦公桌可以隨時從後面拿錢,所以它永遠會有 50。我們將會為這件事做我們自己的型別,所以它也永遠會有 50。這裡是它怎麼做到的:
use std::mem; use std::ops::{Deref, DerefMut}; // 我們將會使用這個來得到 u32 的威力 struct Bank { money_inside: u32, money_at_desk: DeskMoney, // 這是我們的 "智慧指標" 型別. 它有自己的預設值, 但他會使用 u32 } struct DeskMoney(u32); impl Default for DeskMoney { fn default() -> Self { Self(50) // 預設值永遠是 50, 不是 0 } } impl Deref for DeskMoney { // 有的這個我們可以使用 * 存取 u32 type Target = u32; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for DeskMoney { // 並且有了這個我們就可以做加減法等等 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Bank { fn check_money(&self) { println!( "There is ${} in the back and ${} at the desk.\n", self.money_inside, *self.money_at_desk // 要用 * 這樣我們才能印出 u32 ); } } struct Robber { money_in_pocket: u32, } impl Robber { fn check_money(&self) { println!("The robber has ${} right now.\n", self.money_in_pocket); } fn rob_bank(&mut self, bank: &mut Bank) { let new_money = mem::take(&mut bank.money_at_desk); // 這裡拿走錢, 並留下 50 因為那是預設值 self.money_in_pocket += *new_money; // 用 * 因為我們可以只加上 u32. DeskMoney 不能加 bank.money_inside -= *new_money; // 這裡一樣 println!("She robbed the bank. She now has ${}!\n", self.money_in_pocket); } } fn main() { let mut bank_of_klezkavania = Bank { // 安排我們的銀行 money_inside: 5000, money_at_desk: DeskMoney(50), }; bank_of_klezkavania.check_money(); let mut robber = Robber { // 安排我們的搶匪 money_in_pocket: 50, }; robber.check_money(); robber.rob_bank(&mut bank_of_klezkavania); // 搶劫, 再來檢查金額 robber.check_money(); bank_of_klezkavania.check_money(); robber.rob_bank(&mut bank_of_klezkavania); // 再做一次 robber.check_money(); bank_of_klezkavania.check_money(); }
會印出:
There is $5000 in the back and $50 at the desk.
The robber has $50 right now.
She robbed the bank. She now has $100!
The robber has $100 right now.
There is $4950 in the back and $50 at the desk.
She robbed the bank. She now has $150!
The robber has $150 right now.
There is $4900 in the back and $50 at the desk.
你可以看到桌子上總是有 50 美元。
prelude
標準函式庫也有 prelude (預先載入的函式庫),這就是為什麼你不用寫像是 use std::vec::Vec
的東西來建立 Vec
。你可以在這裡看到所有這些元素,並且已經大致瞭解他們:
std::marker::{Copy, Send, Sized, Sync, Unpin}
。你以前沒有見過Unpin
,因為幾乎每一種型別都會用到它(比如Sized
,也很常見)。"Pin" 的意思是釘住不讓東西動。在這種情況下,Pin
意味著它不能在記憶體中移動,但大多數都有Unpin
,所以可以移動。這就是為什麼像std::mem::replace
這樣的函式能用,因為它們沒有被釘住。std::ops::{Drop, Fn, FnMut, FnOnce}
。std::mem::drop
。std::boxed::Box
。std::borrow::ToOwned
。你之前在Cow
有看到過一些,它可以把內容從借來的變成擁有所有權的。它使用.to_owned()
來做到這件事。你也可以使用.to_owned()
在&str
上來得到String
,對於其它的借來值用法也一樣。std::clone::Clone
。std::cmp::{PartialEq, PartialOrd, Eq, Ord}
。std::convert::{AsRef, AsMut, Into, From}
。std::default::Default
。std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator}
。我們之前在疊代器用過.rev()
:實際上是做出了DoubleEndedIterator
。ExactSizeIterator
只是類似於0..10
的東西:它已經知道自己的.len()
是 10。其他疊代器肯定是不知道它們的長度。std::option::Option::{self, Some, None}
。std::result::Result::{self, Ok, Err}
。std::string::{String, ToString}
。std::vec::Vec
。
如果你因為某些原因不想要有 prelude 怎麼辦?就加上屬性 #![no_implicit_prelude]
。讓我們來試一試,看編譯器抱怨什麼:
// ⚠️ #![no_implicit_prelude] fn main() { let my_vec = vec![8, 9, 10]; let my_string = String::from("This won't work"); println!("{:?}, {}", my_vec, my_string); }
現在 Rust 根本不知道你在嘗試做什麼:
error: cannot find macro `println` in this scope
--> src/main.rs:5:5
|
5 | println!("{:?}, {}", my_vec, my_string);
| ^^^^^^^
error: cannot find macro `vec` in this scope
--> src/main.rs:3:18
|
3 | let my_vec = vec![8, 9, 10];
| ^^^
error[E0433]: failed to resolve: use of undeclared type or module `String`
--> src/main.rs:4:21
|
4 | let my_string = String::from("This won't work");
| ^^^^^^ use of undeclared type or module `String`
error: aborting due to 3 previous errors
因此對於這個簡單的程式碼,你需要告訴 Rust 去使用叫做 std
的 extern
(外部) crate,以及你想要用的元素。這裡是一切我們所需要做的事,只是為了建立 Vec 和 String 並印出它:
#![no_implicit_prelude] extern crate std; // 現在你需要告訴 Rust 你想要用叫做 std 的 crate use std::vec; // 我們需要 vec 巨集 use std::string::String; // 還有 String use std::convert::From; // 和這個來轉換 &str 到 String use std::println; // 還有這個來列印 fn main() { let my_vec = vec![8, 9, 10]; let my_string = String::from("This won't work"); println!("{:?}, {}", my_vec, my_string); }
現在終於成功印出 [8, 9, 10], This won't work
。所以你可以明白為什麼 Rust 要用 prelude 了。但如果你願意,你不需要使用它。而且你甚至可以使用 #![no_std]
(我們曾經看過一次),用在你連堆疊記憶體這種東西都無法使用的時候。但大多數時候,你根本不用考慮是否不用 prelude 或 std
。
那為什麼之前我們沒有看過 extern
這個關鍵字呢?是因為你已經不再那麼需要它了。以前在引進外部 crate 時,你必須使用它。所以過去要用 rand
,你必須要寫成:
#![allow(unused)] fn main() { extern crate rand; }
然後用 use
陳述式來表示你想要使用的模組、特徵等等。但現在 Rust 編譯器已經不需要這些幫助了──你只需要使用 use
,Rust 就知道在哪裡可以找到它。所以你幾乎再也不需要 extern crate
了,但在其他人的 Rust 程式碼中,你可能仍然會在頂部看得到它。
時間
std::time
是你可以找到時間相關函式的地方。(如果你想要更多的功能,有 chrono
這樣的 crate 可以用。) 最簡單的功能就是用Instant::now()
取得系統時間。
use std::time::Instant; fn main() { let time = Instant::now(); println!("{:?}", time); }
如果你印出來,你會得到這樣的東西:Instant { tv_sec: 2738771, tv_nsec: 685628140 }
。那裡講的是秒和奈秒,但用處不大。比如你看 2738771 秒(寫於 8 月),就是31.70 天。這和月份、日數沒有任何關係。但是 Instant
的頁面告訴我們,它對本身不應該有用。它說它是 "不透明的(Opaque),只有和 Duration 一起才有用"。這裡不透明的的意思是"你無法搞清楚",而 Duration 的意思是"過去多少時間"。所以它只有在做比較時間這樣的事情時才有用。
如果你看頁面左側的特徵,其中一個是 Sub<Instant>
。也就是說我們可以用 -
來減去另一個。而當我們點選 [src] 看它做了什麼時,它說:
#![allow(unused)] fn main() { impl Sub<Instant> for Instant { type Output = Duration; fn sub(self, other: Instant) -> Duration { self.duration_since(other) } } }
因此,它需要 Instant
,並使用 .duration_since()
給出 Duration
。讓我們試著把它印出來。我們將做出兩個直接相鄰的 Instant::now()
,然後再讓程式忙碌一下。然後我們再多做出一個 Instant::now()
。 最後我們將看看花了多長時間。
use std::time::Instant; fn main() { let time1 = Instant::now(); let time2 = Instant::now(); // 這兩個直接相鄰 let mut new_string = String::new(); loop { new_string.push('წ'); // 讓 Rust 把喬治亞字母推到 String 上 if new_string.len() > 100_000 { // 直到它長達 100,000 位元組 break; } } let time3 = Instant::now(); println!("{:?}", time2 - time1); println!("{:?}", time3 - time1); }
會印出類似這樣:
1.025µs
683.378µs
所以這只是 1 微秒多對上 683 毫秒。我們可以看到 Rust 確實花了一些時間來做。
然而我們可以只用一個 Instant
來做一件有趣的事情。我們可以用 format!("{:?}", Instant::now());
把它轉換成 String
。看起來像這樣:
use std::time::Instant; fn main() { let time1 = format!("{:?}", Instant::now()); println!("{}", time1); }
那會印出類似 Instant { tv_sec: 2740773, tv_nsec: 632821036 }
的東西。那沒什麼用,但是如果我們使用 .iter()
和 .rev()
以及 .skip(2)
,我們可以跳過尾端的 }
和
。我們可以用它來做出隨機數產生器。
use std::time::Instant; fn bad_random_number(digits: usize) { if digits > 9 { panic!("Random number can only be up to 9 digits"); } let now = Instant::now(); let output = format!("{:?}", now); output .chars() .rev() .skip(2) .take(digits) .for_each(|character| print!("{}", character)); println!(); } fn main() { bad_random_number(1); bad_random_number(1); bad_random_number(3); bad_random_number(3); }
會印出類似這樣:
6
4
967
180
這個函式被稱為 bad_random_number
,因為它不是個非常好的隨機數產生器。Rust 有更好的 crate,可以用比 rand
更少的程式碼做出隨機數,比如 fastrand
。但這是個你如何可以利用你的想像力透過 Instant
來做一些事情的好範例。
當你有個執行緒運作時,你可以使用 std::thread::sleep
使它停止一段時間。當你這樣做時,你必須給它 duration。你不必做出多個執行緒來做這件事,因為每個程式至少運作在一個執行緒上。然而 sleep
需要 Duration
,所以它可以知道要睡多久。你可以像這樣選擇單位:Duration::from_millis()
、Duration::from_secs
等等。這裡舉個例子:
use std::time::Duration; use std::thread::sleep; fn main() { let three_seconds = Duration::from_secs(3); println!("I must sleep now."); sleep(three_seconds); println!("Did I miss anything?"); }
只會印出:
I must sleep now.
Did I miss anything?
但執行緒在三秒鐘內什麼也不做。當你有很多執行緒需要經常嘗試一些事情時,比如連線,你通常會使用 .sleep()
。你不希望執行緒使用你的處理器在一秒鐘內嘗試十萬次,而你只是想讓它有時檢查一下。所以你就可以設定 Duration
,它就會在每次醒來的時候嘗試做它的任務。
其他巨集
讓我們再來看看一些其他巨集。
unreachable!()
這個巨集有點像 todo!()
,除了它是針對你永遠不會用的程式碼。也許你在列舉中有個 match
,你知道它永遠不會選擇其中的某個分支,所以程式碼永遠無法到達。如果是這樣,你可以寫 unreachable!()
,這樣編譯器就知道可以忽略這部分。
例如,假設你有個程式,當你選擇一個地方居住時,它會寫一些東西。在烏克蘭除了車諾比外,其他地方都不錯。你的程式不讓任何人選擇車諾比,因為它現在不是個居住的好地方。但是這個列舉是很早以前在別人的程式碼裡做的,你無法更改。所以在 match
的分支中,你可以在這裡用這個巨集。看起來像這樣:
enum UkrainePlaces { Kiev, Kharkiv, Chernobyl, // 假裝我們不能改變列舉 - 車諾比會永遠在這 Odesa, Dnipro, } fn choose_city(place: &UkrainePlaces) { use UkrainePlaces::*; match place { Kiev => println!("You will live in Kiev"), Kharkiv => println!("You will live in Kharkiv"), Chernobyl => unreachable!(), Odesa => println!("You will live in Odesa"), Dnipro => println!("You will live in Dnipro"), } } fn main() { let user_input = UkrainePlaces::Kiev; // 假裝使用者輸入是來自一些其它函示. 無論如何使用者不能選擇車諾比 choose_city(&user_input); }
會印出 You will live in Kiev
。
unreachable!()
對你來說也很好讀,因為它提醒你程式碼的某些部分是不能到達的。不過你必須確定程式碼實際上是到達不了的。如果呼叫了 unreachable!()
,程式就會恐慌。
此外,如果你曾經有到達不了的程式碼,而編譯器知道,它就會告訴你。這裡是個便捷的範例:
fn main() { let true_or_false = true; match true_or_false { true => println!("It's true"), false => println!("It's false"), true => println!("It's true"), // 哎呀, 我們又寫了 true } }
它會說:
warning: unreachable pattern
--> src/main.rs:7:9
|
7 | true => println!("It's true"),
| ^^^^
|
但是 unreachable!()
是用於編譯器無法知道的時候,就像我們的另一個範例。
column!
、line!
、file!
、module_path!
這四個巨集有點像 dbg!()
,因為你只是把它們放進程式碼來給你除錯資訊。但是它們不需要接受任何變數——你只需要把它們和括號一起使用,而且沒有其他東西。它們放到一起很容易學:
column!()
給你寫的那一列file!()
給你寫的檔案名稱line!()
給你寫的那一行,然後是module_path!()
給你模組所在的位置。
接下來的程式碼會在簡單的例子中秀出這三者。我們將假裝有更多的程式碼(模組裡面的模組),因為那就是我們要使用這些巨集的原因。你可以想像 Rust 大程式,它有許多模組與檔案。
pub mod something { pub mod third_mod { pub fn print_a_country(input: &mut Vec<&str>) { println!( "The last country is {} inside the module {}", input.pop().unwrap(), module_path!() ); } } } fn main() { use something::third_mod::*; let mut country_vec = vec!["Portugal", "Czechia", "Finland"]; // 做一些事情 println!("Hello from file {}", file!()); // 做一些事情 println!( "On line {} we got the country {}", line!(), country_vec.pop().unwrap() ); // 做多一些事情 println!( "The next country is {} on line {} and column {}.", country_vec.pop().unwrap(), line!(), column!(), ); // 很多很多的程式碼 print_a_country(&mut country_vec); }
印出這樣:
Hello from file src/main.rs
On line 23 we got the country Finland
The next country is Czechia on line 32 and column 9.
The last country is Portugal inside the module rust_book::something::third_mod
cfg!
我們知道你可以使用 #[cfg(test)]
和 #[cfg(windows)]
這樣的屬性來告訴編譯器在某些情況下該怎麼做。當你有 test
時,當你在測試模式下執行Rust 時,它會執行程式碼(如果是在電腦上,你輸入 cargo test
)。而當你使用 windows
時,如果使用者使用的是 Windows,它就會執行程式碼。但也許你只是想根據不同作業系統對依賴系統的程式碼做很小的修改。這時候這個巨集就很有用了。它回傳 bool
。
fn main() { let helpful_message = if cfg!(target_os = "windows") { "backslash" } else { "slash" }; println!( "...then in your hard drive, type the directory name followed by a {}. Then you...", helpful_message ); }
取決於你的系統這將以不同的方式列印。Rust Playground 在 Linux上執行,所以會印出:
...then in your hard drive, type the directory name followed by a slash. Then you...
cfg!()
適用於任何一種配置。這裡的範例是當你在測試中使用函式時,它的執行方式會有所不同。
#[cfg(test)] // cfg! 會知道要尋找 test 這個字 mod testing { use super::*; #[test] fn check_if_five() { assert_eq!(bring_number(true), 5); // bring_number() 函式應該回傳 5 } } fn bring_number(should_run: bool) -> u32 { // 這個函式接受 bool 作為是否他應該執行的條件 if cfg!(test) && should_run { // 如果它應該執行並且有組態測試就回傳 5 5 } else if should_run { // 如果它不是 test 但它應該執行, 印出某些東西. 當你執行測試它會忽略 println! 陳述式 println!("Returning 5. This is not a test"); 5 } else { println!("This shouldn't run, returning 0."); // 否則回傳 0 0 } } fn main() { bring_number(true); bring_number(false); }
現在根據組態的不同,它的執行方式也會不同。如果你只是執行程式,它會給你這樣的結果:
Returning 5. This is not a test
This shouldn't run, returning 0.
但如果你在測試模式下執行它 (cargo test
,用你電腦上的 Rust 跑),它實際上會執行測試。因為在這種情況下,測試總是回傳 5,所以它會通過。
running 1 test
test testing::check_if_five ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out