Rust程序設計語言(7)

前言

最近真的有點焦慮啊, 難受了

Rust 的標準庫中有一些我們常用的數據結構, 幫助我們更快更好的開發代碼, 這種數據結構被稱爲集合, 大部分其他的數據結構, 比如int大多數只能代表一個值, 而集合可以有多個值

當然我們之前提到的 數組, 元組, 也是可以存儲多個值, 但是他們是將數據存儲在 棧 上的, 之前我們說過, 棧上的數據是需要在分配時就指定其大小, 所以對於動態的可變的數據集合, 最好還是存在堆上, 集合就是這樣, 所以一般而言, 使用本篇介紹的集合結構的時候通常比較多, 本篇介紹3個在 Rust程序中被廣泛使用的集合

  • vector可以一個接一個的存儲一系列數量可變的值
  • 字符串(string)是字符的集合
  • 哈希 map(hash map)可以將值和特定的鍵綁定, 和 pythondict 以及 golangmap 類似

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_strpush來追加字符串

使用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)
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章