透過更嚴謹的檢查在編譯時期就能除去許多錯誤發生的可能,讓程式在執行時期更加安全穩定,和許多程式語言不同, rust 要通過編譯就非常困難,但執行時期會發生的許多錯誤都能因此避免,是一個非常適合撰寫底層程式的程式語言。
Rust 當中變數預設是 immutable ,也就是一但變數初始化之後其值就不能更改,用以下的程式為例
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
若嘗試編譯會得到錯誤訊息,因為 x
的值是不能更改的。這樣的預設可以避免大量執行時期可能的錯誤,程式不被允許任意更改變數的值。 Rust 保證在你給一個變數賦值後,它絕不會改變。
如果想要讓你的變數是可以改變值的,則可以在變數定義時加上 mut
如以下
fn main() {
- let x = 5;
+ let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
constant 在 rust 當中和 variables 預設的行為類似,但依舊有一些不同的點
mut
將 constant 變更為可變的在程式的執行時期 constant 在他們被定義的 scope 以內都是有效的。
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
我們可以重複定義相同名稱的變數,後面定義的變數會將前面的變數 overshadow ,也就是編譯器之後都看見第二個變數。
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
輸出會是
The value of x in the inner scope is: 12
The value of x is: 6
善用這個機制我們可以對某個變數進行多個不同的操作後依然能保存最初的值。注意重新定義相同名稱的變數和使用 mut
來定義變數是完全不同的,重新定義的變數是一個全新且和原本變數的儲存空間不同的變數。
shadowing 機制讓我們不需要為類似用途的變數使用不同的命名,不過 mut
變數不能夠變更其資料型態。
Rust 是一個 statically typed language ,也就是在編譯時期它就必須知道所有變數的資料型態,通常編譯器可以自行推斷該變數的型態,但若多種可能存在,我們需要自行加上 type annotation 。
代表 single value ,主要有四種
Integer
主要分為以下幾種
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
isize
和 usize
取決於運作該程式的電腦架構
Floating-point
同樣遵守 IEEE-754 standard , f32
為 single-precision , f64
為 double precision 。
Rust 可以做不同 scalar type 之間的運算嗎?
Character
char
在 rust 當中的實體由單引號定義如下
let c = 'z';
如果是 string 才用雙引號, char
的大小為 4 bytes ,而且使用的是 unicode ,比起 ascii 它可以表示更多種不同語言甚至是表情符號,而在 unicode 當中其實並不存在 character ,詳細可以參見 Storing UTF-8 Encoded Text with Strings
可以把數個值集合起來變成一個型別, Rust 有兩種原生的 compound types
Tuple
簡單的來說 Tuple 是一個可以包含數個不同資料型別數值的 compound type ,特色是它的長度是固定的,一但定義後我們就不能改變它的大小。
我們可以利用以下方式建立一個 tuple
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
而取出 tuple 當中特定元素的方法可以利用 pattern matching 如下
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
另外也可以利用 .
加上想要取得的元素的 index
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
不包含任何值的 tuple 有一個特別的別名,稱為 unit 。它的資料值和型別都是 ()
,代表 empty value 和 empty return type , Rust 當中的 expression 若不回傳任何值的話預設會回傳一個 unit 。
Array
Array 當中所有值的資料型別必須相同,而長度也是從定義之後就不能改變, Array 的資料會被放在 stack 而非 heap 上,如果希望可以改變大小,可以考慮改用 vector 。
定義 array 的方式有數種,例如以下
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
let a: [i32; 5] = [1, 2, 3, 4, 5];
// a = [3, 3, 3, ,3 ,3]
let a = [3; 5];
Invalid memory access
以下提供一個 rust 當中無效的記憶體操作
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
上述程式編譯會成功,但若輸入的 index 是 5 ,則會跳出錯誤訊息告訴我們 runtime error 的發生,而原因是我們嘗試讀取超出該 array 範圍的記憶體區段,在許多程式語言當中若編譯過了就有機會讀取到違法區域,但 rust 不允許這種行為。
函式定義透過 fn
與命名來完成,注意 rust 使用 snake case 來命名函式與變數,也就是所有字元必須小寫,並利用 _
來區分不同單字。
Rust 不在乎函式定義的先後順序,只要被定義的函式即可呼叫。
至於函式的參數也會作為函式的 signature 的一部分,也就是我們必須清楚定義每個參數的資料型別,例如以下
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
在 Rust 當中清楚定義了 statements 和 expressions 的差別
我們把以下的行為稱為一個 statement
fn main() {
let y = 6;
}
定義函式同樣也是 statement ,上述整段程式碼本身就是一個 statement ,特別注意 statement 是沒有回傳值的,所以我們不能將 let
statement 指派給另一個變數,例如以下行為
fn main() {
let x = (let y = 6);
}
此行為在編譯時期就會被制止,這和 C 很不同,在 C 語言當中我們可以使用 x = y = 6;
,但 Rust 不行。
Expression 則會運算出一個值,例如 5 + 6
是一個會算出 11
的 expression , expression 可以是 statement 的一部分,例如 let y = 6;
即包含一個會算出 6
的 expression 。呼叫 macro 同樣是一個 expression ,透過 brackets 建立新的 scope block 也是一個 expression 。
來探討一下下列程式碼
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
y
後面接的 scope block 就是一個 expression ,最終計算出的 4
會指派給 y
,而 x + 1
後方可以注意到它少了 ;
,如果在此處加上 ;
這個 scope 就會是 statement 而非 expression 也就不會回傳值。
函式的回傳值不需要特別命名,但是一定要將資料型別寫清楚,透過在函式名稱後方使用 ->
來定義,例如以下一個簡單的函式回傳 5
。
fn five() -> i32 {
5
}
如果我們撰寫某個函式如下
fn plust_one(x: i32) -> i32 {
x + 1;
}
編譯後會得到錯誤訊息,由於我們的函式最後沒有計算出一個回傳值 (沒有進行 evaluation ),所以只是一個 statement ,應該把 ;
拿掉。
if
的寫法和其他程式語言大同小異,特別該注意的地方是如果我們寫出以下的程式碼
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
如果這樣直接編譯的話會出錯,由於 rust 不會將 non-Boolean types 自動轉為 Boolean ,我們在 if
後面必須清楚的加上 Boolean 來作為它的條件,上述的程式碼我們必須寫成
- if number {
+ if number != 0 {
另外在 let
statement 當中也可以使用 if
,例如以下
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
}
不過此處要特別注意,這種用法我們要保證 if
導向的兩種可能變數類型必須相同,不可以一邊是整數另一邊是字元。因為值可以在執行時期決定,但資料型態在編譯時期就要確定。
fn main() {
let mut i = 0;
loop {
i = i + 1;
if i >= 10 {
break;
}
}
}
甚至可以利用 loop
來回傳值,例如以下
fn main() {
let mut counter = 0;
let result = loop {
count += 1;
if counter == 10 {
break counter * 2;
}
}
println!("The result is {result}");
}
另外還需要特別探討巢狀迴圈,其他程式語言和 rust 其中一個不同也在此處, rust 可以為內層或外層的 loop 特別做 label ,並且利用 break
或 continue
加上 label 可以特別指定對於哪一層迴圈做控制,我們用以下程式做示範
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
外層迴圈的 label 稱為 counting_up
,若變數 count == 2
則該 break
statement 會將外層迴圈而非只有內層迴圈停止,這有機會讓程式邏輯變複雜很多。
while
後面可以接 Boolean 來判斷此迴圈是否繼續執行。
如果我們想歷遍一個 array 的話可以利用 while
,但很容易寫出有錯誤的程式碼,由於我們要處理 index 的問題,而且速度會因為每次迴圈都要進行判斷而產生的 runtime code 變慢。
若利用 for
可以更快速並安全的歷遍整個 array 如下
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
這樣的程式碼可以讓我們在 array size 改變後也不需要改變程式碼,會好維護很多,在 rust 當中最常被使用的迴圈就是 for
,要進行 countdown 的操作需要搭配 Range
,例如以下
fn main() {
for number in (1..4).rev() {
println!("{number}");
}
}
到此處為止就是 Rust 和其他程式語言共通語類似的地方。
Ownership 算是 Rust 最特別的 feature ,它使得 rust 可以更安全的處理 memory 操作,且不需要 garbage collector ,所以什麼是 ownership ?
Ownership 代表的是 Rust 程式如何管理記憶體的規則集合。每種程式語言都會有自己的規範,來規定運作時如何運用電腦的記體體。主要有以下三種
Stacks and Heap
多數程式語言不會需要開發者對於 stack 或 heap 有特別的認識,但 Rust 需要,一個變數值是放在 stack 或 heap 會很大程度的影響程式行為與如何做某些決策。
Stack 和 Heap 都是程式在執行期間可利用的記憶體區段,但他們的結構不同, stack 儲存與釋放值的方式遵守 LIFO ,所以放在 stack 上的資料大小都必須是在編譯時期就已知的,若無法知道該資料大小則可能被放到 heap 區段。
Heap 不像 stack 的結構如此嚴謹,當我們將資料放入 heap 時,我們向 heap 請求一塊記憶體區塊, memory allocator 會分配一塊足夠大的記體體區段給我們,並將它標示為 use 然後回傳一個指標,也就是該記憶體區段的地址。這樣的過程稱為 allocating on the heap 或 allocating 。而指向 heap 的指標的資料型態是固定並已知的,所以該指標可以被放在 stack 當中。
對 stack 進行 push 比起對 heap 進行 allocating 快,因為 allocator 不需要搜尋可用的記憶體區段,永遠都在 stack 最上方,同樣的存取 heap 當中的資料也比存取 stack 的資料慢,因為我們必須先存取指標,再循著該指標指向的位置去找要存取的記憶體區段。 Processor 處理任務時若要使用的資料之間互相接近,會比很遠的資料更有效率。
因此我們寫程式時最好盡量少把資料放在 heap 上,並且不再使用的 heap 區段記憶體應該被釋放,以此確保我們不會將 heap 記憶體用光, owndership 是禁止這件事發生的,一但完整了解 ownership 後我們不需要太過考慮 stack 和 heap 的事,但先了解可以知道為何 ownership 存在,最主要的功能就是為了管理 heap data 。
首先我們記得三件事
scope 代表的是某個物件在程式當中的有效範圍,用以下程式為範例
{
let s = "hello";
}
也就是說,當 s
進入該 scope 時,它開始生效,直到程式離開該 scope 。從這裡為起點我們先認識 String 這個資料型態。
String 相較前段 Data Types 提及的資料型態更為複雜,它在編譯時期無法先知道大小,並且會被放在 heap 上,因此 rust 必須有一個機制來清理 String 資料。
String
和 string literals 是不同的資料型態,此處我們提及的是 String
,我們可以創建一個 String
如下
let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s);
為什麼 String
可以變為 mutable 但 string literals 不行?這是由於它們處理記憶體的方式不同。
以 string literal 來說,我們在編譯時期就知道它的內容與大小,而這是由於這個資料型別是 immutable 。為了支援 mutable 的字串, String
就誕生了,不但是 mutable ,還可以改變配置在 heap 上的記憶體區段大小,大小在 compile time 未知,這代表兩件事。
String
時能夠釋放該區段記憶體的機制第一個部分由開發者自己做,例如我們呼叫 String::from
,但第二點和許多程式語言不同, Rust 會自動幫我們將該記憶體歸還,當該變數離開它有效的 scope 時。
{
let s = String::from("hello");
}
當程式離開下方括號時,也代表離開 s
有效的 scope ,此時 rust 會自動幫我們把 s
佔據的記憶體空間歸還。歸還的方式是它會呼叫一個函式稱為 drop()
。
這樣的設計對於 Rust 程式來說有很大的影響,目前看起來可能讓程式撰寫變簡單,但當程式碼變得複雜時情況就可能變得不可預期。
Variables and Data interacting with Move
首先我們看一個簡單的例子
let x = 5;
let y = x;
首先把 5 指派給 x
,之後複製一份 x
當中的值給 y
,於是我們在 stack 上就有兩個變數 x
和 y
。
但如果變數變成 String
情況就會很複雜了
let s1 = String::from("hello");
let s2 = s1;
第一行很簡單, memory allocator 在 heap 當中分配一塊記憶體區塊並把 hello
填入,之後回傳一個指標指向該記憶體區段的開頭給 s1
。如下圖所示
那第二行到底做了什麼呢?有兩種可能,一種像整數的情況一樣,系統會為將 x
指向的記憶體內容複製後配置一塊記憶體空間給 y
並把內容填入,如下圖
但如果程式這麼做的話會太浪費,太佔據記憶體空間,所以有另一個作法,就是把 s2
也指向 s1
指向的記憶體區段
實際上 rust 就是採取下面的做法,但我們再回想, s1, s2
目前的 scope 是相同的,而 rust 在離開 s1, s2
有效的 scope 時會自行呼叫 drop()
來將 s1, s2
指向的 heap 記憶體區段釋放,但目前 s1, s2
指向的是同一塊記憶體區塊,如此一來會出現 double free 而造成錯誤!
因此 rust 採取的解決方法是,當 let s2 = s1;
生效時, s1
也自動失效,如此一來離開該 scope 時程式就不會嘗試釋放 s1
指向的記憶體空間。如果編譯以下程式會發生錯誤
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
從上面的描述聽起來, rust 對於 String
進行的複製行為更接近 shallow copy ,而非 Deep copy ,但它會將第一個變數變為無效,因此這個操作稱為 move 。
由此我們可以知道另外一件事, Rust 永遠不會自動進行 Deep copy ,因此任何預設的複製行為都可以被考慮成是低成本的操作。
但如果我們想進行的確實是 Deep copy 呢?這時候我們要利用一個特別的 method 稱為 clone()
,例如以下
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
如此一來 s1
就不會被無效化了,而對於 stack data 來說,由於它們都大小在編譯時期就已知,所以 deep copy 成本很低,因此在 rust 當中 stack data 並不會區分 deep copy 或 shallow copy ,全都會完整地進行複製,就算呼叫 clone()
也不會和原本行為有任何差異。
還有一個 annotation 稱為 Copy
,若某個型別有實作 Copy
,則它的變數不會 move ,只會進行 copy ,使得它們被 assign 到其他變數後還是有效。不過特別注意的是如果該型別已經實作 Drop
trait ,則無法實作 Copy
trait 。
通常只有 simple scalar value 可以實作 Copy
,所有需要多餘的 allocation 的型別都不能實作 Copy
。以下列出幾個有 Copy
trait 的型別
(i32, i32)
可以, (i32, String)
不行我們也需要認識到,將參數傳遞到函式的過程也牽涉到 move 或 copy ,就像 assignment 一樣,我們用下面的程式碼為範例。
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
在 s
被傳入函式的同時, move 也發生了,因此 s
會被無效化,當 takes_owndership(s)
結束後, s
就不再有效。但反之因為 x
是整數型態,它傳送進入函式時發生的是 copy ,因此就算 makes_copy
結束, x
依然有效。
同樣的事情在函式具有回傳值時也相同,若回傳的是 String
這類會進行 move 的型別,則 ownership 會轉移,若回傳的是整數一類的 scalar type ,則只是進行 copy 。變數的 ownership 總是遵守一條法則:將值指派給另一個變數即是 move ,且當該變數離開 scope 時,它的值會被 drop
所釋放。
但這會帶給開發者很大的不便,每次把參數傳入函式當中 ownership 就發生轉移,為了在函式使用完還能使用這些參數,我們還得將他們全部回傳如此一來才能再度取得她們的 ownership ,我們可以利用 tuple 做到一次回傳多個值,不過這非常麻煩,因此我們可以善用 references 。
Reference 的概念和指標很類似,它們都會指向一段儲存資料的記憶體位址,而該值則是被其他變數持有,和指標不同的是, reference 保證在它的 lifetime 都會指向一個有效值並且是特定型別。
例如我們可以撰寫一個計算 String
型別變數的長度的函式如下
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is [].", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
此處我們把參數 &s1
傳入時是利用 reference ,所以不會發生 ownership 轉移,概念如下圖
我們將建立 reference 的操作稱為 borrowing ,但此處有個重點要注意, reference 的值同樣是 immutable 所以預設上我們無法在 borrowing 後改變 referece 指向的值。
若想改變指向的值,可以利用 mutable reference 如下
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
首先在變數的定義上就要設為 mut
,函式的定義當中參數也需要加上 &mut
來表示這是個 mutable reference ,代表 change
這個函數會更動 reference argument 的值。
特別注意 mutable reference 有個限制,如果已經對於某個變數建立一個 mutable reference 那就不能再有指向該變數的 reference 。例如以下的用法會出現編譯錯誤
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
即便是 immutable reference 和 mutable reference 之間也有此限制
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
我們可以同時擁有多個 immutable references ,由於它們之間無法互相影響,但不能在此當中有任何 mutable reference 。
這樣的限制可以在編譯時期就避免 data races , data races 和 race condition 類似,主要是指以下三種情況發生時
如果想讓上述程式碼通過編譯,可以透過加上一個 brackets 來創建一個新的 scope ,使得兩個 mutable reference 在不同階段有作用。
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
另一個特別之處是 reference 的 lifetime 是從建立開始,一直到第一次被使用後,所以以下的程式碼能通過編譯
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
只要 reference 的作用範圍不重疊就可以運作。
Rust 的編譯器保證我們永遠不會產生 dangling reference ,只要對於某個變數值來說還存在指向它的 reference ,編譯器就會確保在該 reference 的 scope 當中變數空間不會被提前釋放。
以下是一個關於 dangling reference 的例子
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!
如果嘗試編譯會得到錯誤,我們嘗試回傳變數 s
的 reference ,但離開 dangle()
函式時,變數 s
就會被 drop()
,原本指向 s
的 reference 會成為 dangling reference ,因此編譯器避免這樣的行為發生。
另一種 reference 型態,也沒有 ownership ,可以用來指向一個 collection 當中連續的 sequence ,但不能指向整個 collection 。
至於 Slice 實際上想解決什麼問題,首先我們思考如果我們將一個 String
當作 input ,想找到該 String
的第一個 word 那該如何做?首先函式的定義就不知道該回傳什麼,因為沒有資料型別用來代表一個 single word
fn first_word(s: &String) -> ?
我們可以改變思考,回傳第一個空格的位置,若沒有空格代表整個 String
就是一個 word ,直接回傳字串長度,實作如下
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
此處的問題是若我們得到一個 usize
的變數後,如果本來 s
的空間被釋放了,編譯依然能通過,但此時 usize
的變數已不再有意義。我們必須有同步 usize
變數和 s
字串的機制。
String slice 是指向 String
某個特定區段的 reference ,如下
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
如此一來我們可以把上述尋找第一個 word 的函式改寫如下
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[..]
}
如果在回傳的 String slice 有效的 scope 內嘗試釋放原本字串的空間,則會出現編譯錯誤。
回想之前我們學過的 string literals
let s = "Hello, world!";
此處 s
的型別是 &str
,是一個指向 binary 的 slice ,所以 &str
實際上也是一個 immutable reference 。
因此上述 first_word
函式的定義也可以改進成
fn first_word(s: &str) -> &str {
如此一來我們可以傳遞 String
或者 string slice 皆可,這樣的彈性稱為 deref coercions 。
array 也可以有 slice ,例如以下
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
slice
型別為 &[i32]
。
struct
和 tuple 十分類似,差別在每個 field 都要清楚命名,至於初始化一個 struct
時,成員順序不一定要和原本定義時相同, struct
的定義更像是一個該型別的 general template 。例如我們可定義與初始化一個 struct User
如下
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
注意到如果 struct
初始化為 immutable 則所有成員皆是 immutable ,若初始化為 mutable 則所有成員皆是 mutable 。我們也可以設計 wrapper function 來初始化 struct
,而成員名稱和函式參數名稱可以相同
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
但當成員數量很多時,不斷的重複命名會很麻煩, Rust 提供一個 Field init shorthand 來幫助我們完成這件事
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
我們也可從一個 struct
instance 建立另一個 struct
instance ,例如以下做法
fn main() {
// --snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
除了 email
成員以外都是從 user1
的成員取得的,所以我們也可以採用另一個寫法
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
..user1
用來表示除了 email
成員以外剩下的成員都使用 user1
的值即可,必須放在最後面。此處其實也是對 user1
當中的成員進行 move 操作,因此 user1
之後便不能再被使用,除非我們使用 Copy
trait 來初始化 user2
。
我們可以定義 struct 但卻不為任何成員命名,像 tuple 一樣,注意每個這樣定義的 struct 都是一個獨立的型別,並不是成員變數型態相同就是同一個型別。 Tuple struct 在我們想為每個 tuple 建立個別的型態與命名,但是成員名稱幾乎都會重複時可以使用。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
此處應該特別注意 black, origin
的型別是不同的。
我們也可以定義沒有任何成員的 struct
,稱為 unit-like structs ,因為它們的行為類似 ()
。
(更多內容在第十章)
Method
類似於 function ,同樣利用 fn
來定義,同樣有參數和回傳值,例如我們可以為 struct Rectangle
定義一個 method 如下
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
此處 impl
包圍的 block 都會和 struct Rectangle
型別關聯在一起,此處 area()
函式的定義中,參數為 &self
,它事實上是 self: &Self
的簡寫,在 impl
block 當中, Self
事實上就是指向 impl
block 指向的型別,每個 method 的第一個參數都會是 self
,型別是 Self
。我們依舊要加上 &
在 self
前面來表示這個 method 只是對 self
進行 borrowing 。如果想要取得 mutable self ,則要標示為 &mut self
。
有時我們會建立一些和 field 成員名稱相同的 method ,稱為 getters ,如此一來我們可以把成員變為 private 而 method 設定為 public ,使得此成員資料為 read-only 。
所有在 impl
block 當中的函式都稱為 associated functions 因為它們都和該 type name 關聯。我們在定義 associated functions 時不一定要將 self
作為一個參數,因為它們可能不需要一個 type instance 就能使用。
不是 method 的 associated functions 經常被用來回傳一個該 struct 的新物件,通常稱為 new
,例如上述的 Rectangle
可以實作以下 new
associated function 。
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
呼叫此函式的方法可以像 let sq = Rectangle::square(3);
。
enums 給我們一個方法來判斷一個值是否是某個集合當中的一個成員,我們以 IP address 為例子來說明,由於一個 IP 位址只可能是 IPv4 或 IPv6 其中一個,不可能兩個都是,此處就適合用 enum 。
enum IpAddrKind {
V4,
V6,
}
接下來我們想要建立兩個實際的 instances 時方法可以如下
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
注意 four
和 six
兩個變數的型態是相同的,都是 IpAddrKind
。
我們也可以定義函式,參數型態就定義為 IpAddrKind
fn route(ip_kind: IpAddrKind) {}
// caller
route(IpAddrKind::V4);
route(IpAddrKind::V6);
於是要表示一個 IP address 時我們可以利用 struct
和 enum
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
以上的程式利用 enum
和 struct
來表示一個 ip address 的值和版本,我們還可以進一步優化它,只需要利用 enum
而不再需要 struct
。
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
此時 enum 的 variant name 變成用來建立 enum instance 的函式! 也就是 IpAddr::V4()
是一個接收 String
型別參數的函式呼叫並回傳一個 IpAddr
instance 。
此實作方法的另一個好處是,每個 variant 可以有不同的型別與數量的參數,例如如果我們想把 IPv4 的位址拆解成四個 0~255 之間的整數,我們可以把 enum
改變為下
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
enum
更可以作為一個很好的函式介面,例如以下
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
此處 enum Message
有四種 variants 並且每個 variant 都接收不同的參數,相同行為我們可以利用 struct
定義如下
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
可以看到我們利用不同的 struct
的話,我們必須定義四種 struct
就無法有像上述一樣統一的介面。
不過 struct
和 enum
也是有類似的地方,例如同樣都可以利用 impl
來定義 method 。
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
Option
EnumOption
是 Rust 標準函式庫當中定義的 enum
,至於它的特色在於如何處理 Null values ,事實上在 Rust 當中是沒有 Null 的存在。而 Null 為何會存在,可以參考 Tony Hoare , null 的發明者在 2009 年演講 "Null Reference: The Billion Dollar Mistake" 當中所述
I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
由於 Null 的存在,程式碼多了很多例外處理以及錯誤的可能性,但我們依舊可以先思考 Null 背後的概念是什麼,主要代表某個某個值當前狀態是無效的或者由於某些理由不存在,主要造成問題的並非這個概念,而是如何實作,因此 Rust 當中並不實作 nulls ,反而利用 enum<T>
來表示某個值是存在或不存在。
enum Option<T> {
None,
Some(T),
}
在程式碼當中我們不需要特別標明 Option
,直接使用 None
或 Some
即可,此處我們僅需明白 None, Some
都是 Option<T>
的 variants 。 <T>
背後有更多的用途和含義,但此處我們只需知道 Option<T>
可以包含任何型態的資料,而每次送入不同型態進入 Option<T>
當中,就代表一個新的型態。
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
上述程式碼就定義了一個型態為 Option<i32>
的變數 some_number
,而對於 absent_number
而言,我們在函式定義時就要寫清楚它的型別,編譯器無法自行推論一個 None
是屬於什麼型態。
而且我們也該注意到 T
和 Option<T>
是完全不同的兩個型別,不能把這兩種型別的變數放在一起操作,例如以下範例
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
嘗試編譯會得到錯誤,由於編譯器把 i8
和 Option<i8>
視為不同型別,此處精妙之處在於只有我們在操做 Option<i8>
時我們才需要考慮它可能不含有有效的值,編譯器在進行運算之前會先確保沒有例外發生,換句話說,我們需要將 Option<T>
轉換為 T
才能進行 T
operations 。這幫助我們排除一個巨大的可能錯誤:假設某個變數是有值的,但實際上沒有。
如果某個變數有可能是 null ,則我們需要清楚地將該變數型別定義為 Option<T>
,並且需要特別處理該變數為 null 的情形。當我們看到任何變數型別並非 Option<T>
時,我們可以安全的假設該變數絕不會是 null 。
match
Control flow constructRust 其中一個強大的 control flow construct 稱為 match
,讓我們可以將一個值拿來和許多 pattern 進行比較,透過 pattern 的 expressiveness 和編譯器來確認所有可能的案例都被分析過。
match
可以被想像成是 coin-sorting machine ,想像一個硬幣落到一個篩選器當中,裡面有許多不同大小的孔洞,而硬幣會掉入第一個能夠容納該硬幣大小的孔洞當中。在 match
當中進行比較的值也利用相同的模式運作,第一個和該值符合的 pattern ,該值會落入該 pattern 指向的 code block 當中運作。
例如我們可以寫一個函式,接收一個硬幣種類並回傳它的價值
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
它看起來和 if
運作的邏輯類似,但有一個很大的差別, if
後面必須加上 Boolean 但 match
可以接上任何型別。 match
experession 執行時,會將送進來的值和分支逐一比較,如果分之當中要執行的程式碼較多,可以送 {}
包裝起來,但依舊會回傳一個最終值。
match
分支甚至可以和 patterns 連結,並且從 enum variants 當中獲得值,例如我們可以把上述的 enum Coin
更改為以下
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
接著我們可以修改 match
當中的判斷式,若 coin
是 Coin::Quarter
此 variant ,就另外將它的 state 取出
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
Option<T>
記得上一章節剛談到 Option<T>
,那其中一個便於將 Option<T>
當中的值取出進行 T
operation 的方法就是 match
,由於我們還要處理它可能是 None
的情況, match
就變得非常適合。
例如我們針對 Option<i32>
撰寫一個加一的函式
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
而如果你忘記考慮 null case 把程式碼寫為以下
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
編譯器也會幫你抓出這個錯誤而讓編譯失敗,而在更複雜的情形當中,編譯器也會強迫你把所有可能的 case 都清楚寫出來。
當然他也有類似 default 的機制,讓你把剩下所有 case 都進行 default 的操作,使用 other
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
同時也可以利用 _
來告訴系統我們對於沒有 match 到的值不需要進行任何處理
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
if let
首先觀察以下程式碼
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
此處 match
的行為是,若 config_max
是 Some
則我們將它印出,若不是我們什麼都不做,我們可以用 if let
讓這段程式碼看起來更簡潔
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
不過這個方法就缺少了 match
的 exhaustive 特性來檢查所有可能的情形,我們可以將 if let
視為 match
的語法糖,只檢查特定一個 pattern 而忽略所有其他可能情形。
多數資料型態一次只能表示一個值,但 collection 卻可以同時包含多個值,它和 arrray 或 tuple 不同, collections 所指向的資料是存放在 heap 區段,代表在編譯時期資料量並不需要事先被知道,而且在執行時期可以變化, collections 有非常多種,成本和容量都不同,視情況選擇非常重要,此處先探討幾個最常見的 collections 。
vector ,在程式當中表示為 Vec<T>
,讓我們可以一次存放多個值,相鄰的值在記憶體當中的分布也是連續的。
建立新的 vector 方式如下,呼叫 Vec::new
函式即可
let v: Vec<i32> = Vec::new();
我們在此處需要特別將 v
的型態寫出來,因為我們尚未把任何值放入 v
當中,編譯器無法得知 v
的型態,如果我們建立 vector 時就有初始值,則編譯器可以自行推斷變數的型態,例如以下
let v = vec![1, 2, 3];
此處編譯器可以推斷 v
的資料型態是 i32
。
但以上方法建立的 vector 都是無法增加或刪減資料的,因為 vector 就像其他所有資料一樣預設都是 immutable ,因此要增加資料需要將 v
設為 mutable 並利用 push()
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
此處由於 push()
進入的元素是 i32
編譯器會自動推斷 v
的型別是 Vec<i32>
。
而讀取 vector 當中的元素主要有兩種方法,透過 index 或者 get
method 。
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
為何 Rust 提供兩種不同讀取 vector 元素的方式呢?讓我們嘗試讀取一個不存在的變數,例如在只有五個元素的 vector 當中讀取 index 100 的值
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
拿去編譯的話,第一個 let does_not_exist = &v[100];
會造成編譯錯誤,因為它嘗試讀取一個根本不存在的元素,但 get
method 則會回傳一個 None
,不會出現 panick 。
另一個需要注意的地方會是 ownership 和 borrowing 的操作, borrow checker 會確保每個針對該 vector 的 reference 都是有效的,而記得我們不能同時有 immutable reference 和 mutable reference 的存在,所以以下程式會編譯錯誤
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
至於編譯失敗的原因需要探討,因為 first
變數是一個指向該 vector 第一個元素的 reference ,在 v.push(6)
的操作當中,若新增元素後當前為 v
配置的記憶體空間不足,系統會配置一塊更大的空間並把 v
所有的資料複製並移動到新的空間,若此情況發生, first
這個 reference 指向的空間就會被釋放,而 borrowing rules 會避免這個可能發生。
我們可以用一個 for loop 簡單的歷遍整個 vector
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
如果想要更改當中的值則需改為 mutable
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
但我們無法在走訪 vector 時移除或插入任何元素,原因跟上述記憶體空間可能被重新配置有關。
另外當一個 vector 因為離開了有效的 scope 而被 drop 時,當中所有的元素也會跟著被釋放。
在 Rust 當中, string 指的是原本程式語言當中包含的 string slice 也就是 str
,通常會使用 reference 搭配使用 &str
。 String
則是在 Rust 的標準函式庫當中,是一個 growable, mutable, owned, UTF08 encoded string type ,通常稱為 strings 。
許多 Vec<T>
能用的操作在 String
當中也都能用,事實上 String
的實作就能算是一個對於 Vec<T>
的 wrapper ,加上一些額外的限制。
建立一個新的 String
就和 Vec<T>
相同
let mut s = String::new();
另外我們可以從 string literals 將 str
的資料轉入 String
。例如以下
let data = "initial contents";
let s = data.to_string();
// or
let s = "initial contents".to_string();
基本上所有具備 Display
trait 的資料型別都可以使用 .to_string()
。另外也可以使用 String::from
。
let s = String::from("initial contents");
String
的大小和內容都可以改變,和 Vec<T>
一樣,我們可以利用 +
operator 或 format!
macro 來串接 String
變數值。
例如我們想在原本的字串後面補上一些字,可以利用以下方法
let mut s = String::from("foo");
s.push_str("bar");
特別注意 push_str()
method 接收的是 string slice 因為我們並不需要取得參數的 ownership ,也就是說送入 push_str
的 string slice 之後還是可以繼續使用
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}");
若只是要新增一個單一字元,可以利用 push
method
let mut s = String::from("lo");
s.push('l');
如果是要串接兩個 String
變數,則有以下幾種方法,第一種是利用 +
operator
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 has been moved here and can no longer be used
在標準函式庫當中, +
operator 的 signature 看起來類似於以下
fn add(self, s: &str) -> String {
add
透過 generics 和 associated types 定義,此處可以看到第二個參數型態是 &str
,所以 s2
經過 add 操作後依舊可以使用,但 s2
的型態是 String
,為何可以通過編譯?原來在 Rust 編譯器當中存在 deref coercion ,會將 &s2
轉為 &s2[..]
,也就是編譯器會強制將 &String
轉為 &str
。
另外該注意的是 let s3 = s1 + &s2
看起來像是將原本字串複製一份,但實際上此處是拿走 s1
的 ownership 並將 s2
串接到它後面,這樣的實作比起複製操作還高效許多。
若我們要串接多個字串,例如以下例子
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
這樣寫很難分辨程式的行為,我們可以利用 format!
macro
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
特別注意 format!
macro 並不會拿走任何參數的 ownership ,所以所有參數在使用 format!
後都還是有效的。
我們應該特別認知到 Rust 當中的字串不支援 indexing ,首先我們要了解 Rust 如何將字串儲存在記憶體當中。
String
實際上是 Vec<u8>
的 wrapper ,例如以下的變數
let hello = String::from("Hola");
此處 hello
的長度會是 4 ,由於 "Hola"
佔據 4 bytes 。再看另一個例子
let hello = String::from("Здравствуйте");
此處第一眼可能會認為它的長度是 12 ,但實際上是 24 ,因為那是這段字串用 UTF-8 加密後佔據的位元組長度,在 UTF-8 當中每個 Unicode 都佔據 2 bytes ,所以一個字串的 bytes 數量和它顯示的長度不一定相同。
如果我們對一個字串進行 indexing 例如以下
let hello = "Здравствуйте";
let answer = &hello[0];
我們希望 answer
取得的值是 3
,但由於 UTF-8 encoding 的關係, 3
佔據了 2 個 bytes ,第一個 bytes 是 208
而第二個 bytes 是 151
。所以 answer
事實上會得到 208
,但 208
並非一個有效的字元,為了避免這種錯誤, Rust 在編譯時期就會避免所有 String
的 indexing 操作。
我們可以透過三種不同面向來解讀 Rust 當中的 String
以下將以印度文的 “नमस्ते” 作為範例,它在電腦當中會以一個 u8
型態的 vector 被儲存如以下
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
總共是 18 bytes 同時也是電腦儲存此資料的形式。若我們將它看作 Unicode scalar value ,在 Rust 當中也就是 char
,這些位元組會變成
['न', 'म', 'स', '्', 'त', 'े']
共有六個 char
值在其中。最後若我們將它視作 grapheme clusters ,我們就能取得人類能解讀的四個印度文字
["न", "म", "स्", "ते"]
Rust 提供我們不同方式解讀一組存在電腦當中的 raw string data 。另外一個 Rust 不讓我們對 String
進行 indexing 的原因,是因為 indexing operation 通常應該在 String
資料型態來說這無法被保證,因為 Rust 會從頭開始一直搜索到該 index 的位置來判斷有幾個有效的字元。
若我們真的需要使用 indices 來建立一個 string slices ,則我們需要提供更多資訊。在 Rust 當中 String
搭配 []
的使用不是一個 single index ,而應該是一個 index range 例如
let hello = "Здравствуйте";
let s = &hello[0..4];
此處 s
的資料型別就會是 &str
並且包含了 hello
的前四個 bytes ,所以 s
會是 Зд
。如果我們的 index 放錯使得某個字元只有一個 byte 被取出,則執行時期會出錯。
在操作 strings 時,最好的方式就是清楚表述出我們希望取得的是字元還是 bytes 。對於 unicode scalar values 來說,使用 chars
method ,例如對 "Зд"
呼叫 chars
methods 會得出兩個資料型別為 char
的值。
for c in "Зд".chars() {
println!("{c}");
}
如果想取得的是 bytes ,則呼叫 bytes
method
for b in "Зд".bytes() {
println!("{b}");
}
這段程式碼會印出
208
151
208
180
我們應該記住的是,一個有效的 Unicode scalar value 可能由一個以上的 bytes 組成。
Hash Maps 的表示法是 HashMaps<K, V>
,透過一個 hashing function 存放 keys of type K
和 values of type V
。以下簡單介紹 Hash Maps 的幾個 API 。
其中一個建立新的 Hash Map 方法即是透過 new
並且透過 insert
新增資料。
use std::collections::HashMaps;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
像 Vector 一樣, Hash Map 同樣將資料存在 heap 區段,而且同樣是 homogeneous 的,所有 key 必須有相同的資料型態,所有 value 也需要擁有相同資料型態。
我們可以透過 get
method 取得 Hash Map 當中的值,例如
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).copied().unwrap_or(0);
get
method 會回傳的資料型態為 Option<&V>
,若沒有和該 key 對應的 value , get
會回傳 None
。這段程式處理 Option
的方法是透過 copied
來取得 Option<i32>
然後利用 unwrap_or
取得當中的值給 score
,若為 None
則會設為 0 。
我們也可以利用 for loop 來走訪整個 Hash Map 如下
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}");
}
把值放入 Hash Maps 時,對於有 Copy trait 的資料型別例如 i32
,它的值會直接被複製進入 hash map ,對於 String
一類的資料型別來說,它們的值則是會被 moved 到 hash map 當中, hash map 會是這些值的 owner 。
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 and field_value are invalid at this point, try using them and
// see what compiler error you get!
更新 HashMap 有許多方法,唯一要遵守的規範是一個 key 只能對應到一個 value ,我們可以用新的 value 替換掉原本的舊 value ,或者保有舊的 value 而忽略新的 value ,只有當該 key 並沒有對應到任何 value 時才加上新的 value 。或者我們可以將新的與舊的 value 合併。
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
entry
,會將想新增的 key 當成參數,回傳值是一個 enum
稱為 Entry
,代表該 value 是否存在,範例如下
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yello")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");
Entry
的 or_insert
method 會回傳一個指向對應 Entry
key 的 mutable reference ,若該 key 不存在則會將新的 value 新增後回傳該 mutable reference 。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:?}");
or_insert
method 會回傳一個 mutable reference &mut V
指向該 key 對應的 value 。預設上 HashMap
使用的 hashing function 是 SipHash
,是一個可以抵抗對於 hash tables 的 Denail of Service (DoS) attacks 的函式。