前言
最近真的有點焦慮啊, 難受了
Rust 的标準庫中有一些我們常用的資料結構, 幫助我們更快更好的開發代碼, 這種資料結構被稱為
集合
, 大部分其他的資料結構, 比如
int
大多數隻能代表一個值, 而集合可以有多個值
當然我們之前提到的 數組, 元組, 也是可以存儲多個值, 但是他們是将資料存儲在 棧 上的, 之前我們說過, 棧上的資料是需要在配置設定時就指定其大小, 是以對于動态的可變的資料集合, 最好還是存在堆上, 集合就是這樣, 是以一般而言, 使用本篇介紹的集合結構的時候通常比較多, 本篇介紹3個在 Rust程式中被廣泛使用的集合
- vector可以一個接一個的存儲一系列數量可變的值
- 字元串(string)是字元的集合
- 哈希 map(hash map)可以将值和特定的鍵綁定, 和
的python
以及dict
golang
類似map
vector
vector 類型是
Vec<T>
, vector 的特點是他的多個值都在記憶體中彼此相鄰的排列在一起, 這樣會提高查找和操作的速度, 一個 vec 下的所有值的類型必須相同
Vec in std::vec - Rust (rust-lang.org)
建立
可以使用
Vec::new
來建立一個空的
Vec
let v: Vec<i32> = Vec::new(); // 建立一個 Vec, Vec 内部的值類型是 i32, 現在還沒有具體的值
這一句的作用是建立一個空的 Vec 類型, 此時因為沒有給 Vec 設定指定的值, 是以 Rust 是不知道這個 Vec 需要存儲的值的類型, 這裡我們在建立時就使用
Vec<i32>
來設定裡面存儲的值的類型
還有一種方法
let v = vec![1, 2, 3]; // 建立一個 Vec. 在建立時就插入3個值 1, 2, 3
注意到,
vec!
這是一個宏, 這個宏會根據我們提供的值來建立 Vector, 同時自己判斷值的類型并給這個 vec 進行設定, 這裡就是自己推斷出是 i32 類型
更新
使用
push
裡可以項 vec 裡增加值
let mut v: Vec<i32> = Vec::new(); // 建立一個 Vec, Vec 内部的值類型是 i32, 現在還沒有具體的值
let mut v1 = Vec::new(); // 空的 vec, 類型還沒有指定
let mut v2 = vec![1, 2]; // vec
v.push(3); // 新增
v1.push(3); // 這裡是先擷取到值的類型, 設定 vec 的類型, 再新增到 v1
v2.push(3); // 新增
push
可以更新 vec 的值, 準确說是追加
這裡的 v1 , 在建立時沒有指定類型, 而是在 push 時靠 rust 自己判斷, 也是可以的
釋放
vector
在其離開作用域時會被釋放掉
{
let v = vec![1, 2, 3, 4];
// 處理變量 v
} // <- 這裡 v 離開作用域并被丢棄
當 vector 被丢棄, 裡面的值也會被丢棄
讀取
讀取 vector 的值可以使用索引和
get
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2]; // 擷取索引2的值
println!("The third element is {}", third);
match v.get(2) { // 使用 get 擷取索引2的值, 沒有就是 None, 這裡使用 match 判斷
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
對于直接擷取索引的方式, 如果索引超出了範圍, 比如隻有3個值, 結果你擷取索引3, 就會導緻程式發生 panic, 直接崩潰
get
, 如果超出索引, 隻會傳回
None
, 是以一般使用 get 來防止程式崩潰
下面再看一個代碼
let mut v = vec![1, 2, 3, 4, 5]; // 可變的 vec
let first = &v[0]; // 借用 v 的第0個值
v.push(6); // 給 v 追加值6
println!("The first element is: {}", first); // 觸發了 panic
這個代碼實際上會報錯
➜ t_vec git:(master) ✗ cargo run
Compiling t_vec v0.1.0 (/Users/Work/Code/Rust/student/t_vec)
warning: unused variable: `first`
--> src/main.rs:4:9
|
4 | let first = &v[0];
| ^^^^^ help: consider prefixing with an underscore: `_first`
|
= note: `#[warn(unused_variables)]` on by default
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/t_vec`
之前在所有權那裡, 你已經知道了, 可變引用和不可變是無法同時存在的, 這裡的 first 是不可變引用, 随後 v 自己進行了追加操作, 而後再列印 first (不可變引用)就觸發了沖突, 為什麼對 v 進行 push 會對所有權進行轉移? 這是因為 vector 之前說過, 裡面的每個值在記憶體中是相鄰的, 但是系統的記憶體配置設定并不受 rust 控制, 會出現這種情況, 本來這個 vec 長度為3, 于是 rust 在記憶體中存儲了長度為3的資料, 此時别的軟體也向系統申請了記憶體, 在你的資料之後, 與你的資料相鄰, 此時你擷取了索引為0的位址, 而後進行 push 操作, 新增一個值, 此時因為記憶體中你的相鄰處已經被其他值占領, 于是rust 隻能再請求一個新的長度為4的位址,把4個值重新放入新的位址保證相鄰, 你再去通路之前的索引為0的位址, 此時這個位址的所有權就不在你的手上了, 是以 rust 不允許你進行操作了.
周遊
如果想要依次通路 vector 中的每一個元素, 我們可以對這個 vec 進行周遊
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
也可以周遊時對齊進行修改
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50; // 值增加50
}
搭配枚舉使用
vector 還有一個友善的特點是, 他也可以存儲相同枚舉的值, 因為他認為枚舉也是同一個類型, 如果我們想要在一個 vec 中存儲不同類型的值, 可以将這些類型設定為同一個枚舉的成員
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
]; // 可行, 此時他的類型是枚舉 SpreadsheetCell
其他
vector 還有很多其他的方法, 比如
pop
可以删除最後一位值, 具體的可以檢視 api 文檔
字元串
我們之前使用過字元串, 而本章我們會深入的了解字元串
什麼是字元串
你真的了解字元串嗎?
rust 中隻有一種字元串類型, 那就是
str
, 對于字元串slice, 他通常是 str 的借用, 也就是
&str
string
類型是标準庫提供的, 并沒有寫進核心語言部分, 他是可以增長的, 可以變動的, 有所有權的, 編碼是 UTF-8的字元串類型
let mut s = String::new();
上面的代碼是建立了一個空的字元串 s, 然後我們可以給 s 填充資料, 但是通常我們會直接初始化失敗時指定資料, 例如
let data = "initial contents"; // 字元串字面值
let s = data.to_string(); // 使用 to_string 方法
// 該方法也可直接用于字元串字面值:
let s = "initial contents".to_string(); // 更加簡單的寫法
let s = String::from("initial contents"); // 更加簡單的寫法2
隻要某個類型實作了
Display
類型, 他就可以使用
to_string
方法來轉換成字元串
對于這兩種簡單寫法, 并沒有什麼優劣, 是以按需使用
rust 中的字元串編碼為
utf-8
, 是以他能放入任何可以正确編碼的資料
string的大小可以增加, 内容也可以修改
push_str
和
push
來追加字元串
push_str
來追加字元串 slice
let mut s = String::from("foo");
s.push_str("bar"); // s 為 foobar
這裡對 s 使用
push_str
, 對 s 進行字元串的追加, 同時, 為了保證所有權不轉移,
push_str
使用的是字元串 slice
push
來追加字元(不是字元串)
let mut s = String::from("lo");
s.push('l'); // s = lol
使用 + 運算符或者 fromat! 宏拼接字元串
你還可以使用
+
友善的組合字元串
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移動了,不能繼續使用
這裡的 s3 會成為
Hello, world!
, 注意代碼
s1 + &s2
, 這是因為
+
使用的函數定義為
fn add(self, s: &str) -> String {
其中, self 是 s1, s 為
&s2
, 這裡要求參數是引用, 避免參數 s 的所有權發生轉移. 其次, 我們注意這個參數類型是 str 的引用, 而 s2是 String 類型, 為什麼能編譯運作呢?
這是因為
&String
可以被強轉成
&str
, 在調用+時, Rust 使用了強制轉換, 将其變成&str
這裡說的 s1的所有權被移動了, 是因為參數
self
擷取了所有權, 此時所有權到了
add
中, 是以下面使用 s1會造成錯誤
還可以使用宏
format!
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
類似于 golang 的 fmt.Printf, 就是格式化字元串
索引字元串
很多類型都可以使用索引來通路其中某個元素, 但是對于字元串, 則是不行的, 字元串并不支援使用索引文法
内部實作
String
是一個
Vec<u8>
的封裝, 比如字元串
Hola
在Rust 中的長度是四個位元組, 這是正确的, 因為每個字母的 utf8 編碼都占用1格子姐, 那麼字元串
дравствуйте
則不同, 字元串
дравствуйте
的長度為22, 這是因為
дравствуйте
的每一個字元需要兩個位元組存儲, 他是unicode 編碼, 但是按照索引來擷取, 是按照位元組去尋址, 那麼問題就出現了, 你擷取
дравствуйте
的索引0, 不是
д
, 而是
д
的一部分, 這就不是你想要擷取到的結果了
是以 rust 為了避免出現問題, 将這個功能屏蔽了
其實 rust 也可以分辨出哪些存儲多少位元組, 而在你擷取索引時對不同情況做特殊的處理, 但是這樣的話勢必會造成性能的損耗, rust 還需要多次的判斷和周遊才能擷取到你想要的結果, 而 Rust 期望擷取值的時間為
(O(1))
使用字元串 slice
如果我們就是想要使用索引, 這裡有一個危險的方法
let hello = "дравствуйте";
let s = &hello[0..4]; // др
擷取
дравствуйте
的前4個位元組, 之前說過俄語是兩個位元組為一個字元, 是以這裡是前兩個字元
如果你擷取的是
[0..1]
, 因為顧頭不顧尾原則, 實際上擷取的是
д
的一部分, 那麼此時會導緻
Panic
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `дравствуйте`', src/libcore/str/mod.rs:2188:4
是以非常不推薦使用這個方法
周遊字元串
Rust 提供了一種方法可以讓你周遊字元串, 這是安全的, 而且是按照字元周遊而不是位元組
fn main() {
let s = String::from("дравствуйте");
for c in s.chars(){
println!("{}", c)
}
}
運作
д
р
а
в
с
т
в
у
й
т
е
而當你想周遊每一個原始位元組, 使用
.bytes()
fn main() {
let s = String::from("дравствуйте");
for c in s.bytes(){
println!("{}", c)
}
}
208
180
209
128
208
176
208
178
209
129
209
130
208
178
209
131
208
185
209
130
208
181
這裡的每個數字都是每個位元組的 ascii 對照
哈希 map
哈希 map 其他語言也有, 比如 golang 的 map, python 的 dict, 在 Rust 中他是
HashMap<k, v>
, 他的結構是一個鍵類型
k
對應一個值類型
v
, 他通過哈希函數來實作兩者的映射管理, 你可以很友善的通過某個 k 找到對應的 v
new
建立一個空的
HashMap
, 使用
insert
來增加元素
use std::collections::HashMap;
let mut scores = HashMap::new(); // hashmap
scores.insert(String::from("Blue"), 10); // Blue: 10
scores.insert(String::from("Yellow"), 50); // Yellow: 50
因為 hashmap 相對于 vector 和 string 來說并不是那麼常用, 是以并沒有預設就導入, 是以需要通過
use std::collections::HashMap;
來導入到目前的代碼中
我們之前說過集合都是将資料存放在堆上的 是以可以友善的進行擴容, 而與 vector 相同的是, 哈希 map 是同質的, 所有的鍵都必須是相同的類型, 值也是
另一種建構哈希 map 的方式調用一個 vector 的
collect
方法, 這個 vector 必須是元組類型, 例如
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();
這裡是使用
zip
将兩個 vector 組合, 再使用
collect
将其轉換成一個 hashmap
HashMap<_, _>
是必須要标記的, 他代表
collect
輸出的結構. 必須要顯式的指定才可以, 其中的
_
代表占位
所有權
hashmap 也有所有權, 對于像
i32
這種實作了
Copy
的 trait 的類型, 其值可以拷貝進哈希 map. 對于像
string
的擁有所有權的值, 其值将被移動到哈希 map 中, 成為這個值的所有者
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 值
get
get
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); // 擷取key Blue 的值
如果 key blue 不存在, 則會傳回
None
循環
使用 for 循環來周遊鍵值對
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 的方式, 是以 hashmap key是無序的
更新哈希 map
覆寫
對于已經存在的 key, 我們可以直接覆寫這個 key 下的值, 直接使用
insert
即可
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores); // 25
隻建立不覆寫
你可以能注意, 使用
insert
會直接覆寫值, 那麼如果我們想隻在這個 key 不存在時才插入的話, 配合使用
entry
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);
println!("{:?}", scores);
entry 傳回了一個枚舉
Entry
, 其有一個方法
or_insert
在建對應的值存在時就傳回這個值的可變引用, 如果不存在就将參數作為新值插入并傳回可變引用
根據舊值更新
比如對值進行+1而不關注這個值本來的值
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); // 這個字元作為 key 不存在就 set 成0
*count += 1; // +1
}
println!("{:?}", map);
之前說過,
entry
不管怎樣都會傳回值的可變引用, 是以我們直接修改這個引用的值即可
擷取可被修改的值
有時候hashmap 重點值存儲的可能是 vector 這種集合, 而我們想要擷取值并進行 push 或者其他的追加操作, 可以使用
get_mut
let mut company = HashMap::new();
company.insert("c1", vec![1, 2]);
let c1 = company.get_mut("c1"); // 擷取值的可變引用
c1.unwrap().push(3) // unwrap 是将類型剝離出來,
get_mut
可以獲得值的可變引用, 以便我們直接對其進行修改
同時,
unwrap
也必不可少, 不使用
unwrap
時, 運作報錯
no method named `push` found for enum `std::option::Option<&std::vec::Vec<{integer}>>` in the current scope
此時可以看出來, c1的類型變成了
std::option::Option<&std::vec::Vec<{integer}>>
, 被包裹在了Option 中, 我們必須要調用
unwrap
将其剝離出來, 類型變回
&std::vec::Vec<{integer}>
練習題
求平均數
給定一系列數字,使用 vector 并傳回這個清單的平均數(mean, average)、中位數(排列數組後位于中間的值)和衆數(mode,出現次數最多的值;這裡哈希 map 會很有幫助)
fn average(v: &Vec<f64>) -> f64 {
let mut sum = 0.0;
for i in v{
sum += i
}
sum / v.len() as f64 // len 傳回長度, 類型是 usize, 通過 as 轉換成 f64
// / 是除
}
fn main() {
let v0 = vec![1.0, 2.0, 3.0, 5.0];
let v1 = vec![-1.0, -2.0, -3.0];
let v0average = average(&v0);
let v1average = average(&v1);
println!("v0 res = {}", v0average);
println!("v1 res = {}", v1average)
}
這裡有之前沒有寫到部落格裡的 vector 的 len 方法, 傳回長度
字元串轉換
将字元串轉換為 Pig Latin,也就是每一個單詞的第一個輔音字母被移動到單詞的結尾并增加 “ay”,是以 “first” 會變成 “irst-fay”。元音字母開頭的單詞則在結尾增加 “hay”(“apple” 會變成 “apple-hay”)。牢記 UTF-8 編碼
fn pig_lation(g: &str) -> String { // 因為 str 必須要在初始化時就要知道其大小, 是以傳回 string
// 轉換成 string
let general = g.to_string();
let mut is_vowel = false;
let vowel = vec!['a', 'i', 'y', 'o', 'u'];
// 擷取首字母, 檢視是元音還是輔音
// 不能粗暴的直接擷取索引0, 需相容其他語言
for i in general.chars(){
for k in &vowel{
if i.to_string() == k.to_string() {
is_vowel = true
}
}
break
}
if is_vowel{
// 首字母是元音
return format!("{}-hey", general)
}else{
let mut p = String::new();
let mut is_first = true;
let mut first_word = String::new();
for i in general.chars(){
if is_first{
// 第一次
first_word = i.to_string();
is_first = false;
continue
}else{
p = p+&i.to_string()
}
}
return format!("{}-{}ay", p, first_word)
}
}
fn main() {
let t0 = "apple";
let t1 = "first";
let t2 = "蘋果";
let r0 = pig_lation(t0);
let r1 = pig_lation(t1);
let r2 = pig_lation(t2);
println!("r0 = {}", r0);
println!("r1 = {}", r1);
println!("r2 = {}", r2)
}
部門控制
使用哈希 map 和 vector,建立一個文本接口來允許使用者向公司的部門中增加員工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着讓使用者擷取一個部門的所有員工的清單,或者公司每個部門的所有員工按照字典序排列的清單。
use std::{io, collections::HashMap}; // 引入标準庫
fn main(){
println!("CRM");
let mut company = HashMap::new();
loop{
println!("輸入所在部門->");
let mut class = String::new(); // 建立一個字元串變量 class
io::stdin() // 調用函數stdin
.read_line(&mut class) // 調用stdin的方法read_line擷取輸入值
.expect("讀取失敗"); // 如果擷取錯誤列印警告
println!("輸入使用者名->");
let mut name = String::new(); // 建立一個字元串變量 name
io::stdin() // 調用函數stdin
.read_line(&mut name) // 調用stdin的方法read_line擷取輸入值
.expect("讀取失敗"); // 如果擷取錯誤列印警告
let ns = company.get_mut(&class); // 擷取可變引用
if ns == None{
company.insert(format!("{}", class), vec![format!("{}", name)]); // 防止所有權轉移, 使用 format 重新制造一個 str
}else{
ns.unwrap().push(name) // 直接 push
}
for i in company.get_mut(&class).unwrap(){
println!("{}", i)
}
}
}
作者:ChnMig
出處:http://www.cnblogs.com/chnmig/
本文版權歸作者和部落格園所有,歡迎轉載。轉載請在留言闆處留言給我,且在文章标明原文連結,謝謝!如果您覺得本篇博文對您有所收獲,覺得我還算用心,請點選左下角的 [推薦],謝謝!