Rust入坑指南:鱗次櫛比

Rust入坑指南目錄:


Rust入坑指南:鱗次櫛比


很久沒有挖Rust的坑啦,今天來挖一些排列整齊的坑。沒錯,就是要介紹一些集合類型的數據類型。“鱗次櫛比”這個標題是不是顯得很有文化?

在 一文中我們已經介紹了一些基本數據類型了,它們都存儲在棧中,今天我們重點介紹3種數據類型:string,vector和hash map。

String

String類型我們在之前的學習中已經有了較多的接觸,但是沒有進行過詳細的介紹。有些有編程基礎的同學可能不屑於學習String類型,畢竟它在所有編程語言中可以說是最常用的類型了,大家也都很熟悉了。對於有這種心理的同學,我想對他們說:我剛開始也是這樣想的,直到後來我被編譯器揍的滿頭包,才下定決心回來認真學習一下String類型。

Rust的字符串分為以下幾種類型:

  • str:表示固定長度的字符串
  • String:表示可增長的字符串
  • CStr:表示由C分配,被Rust借用的字符串,一般用於和C語言交互
  • CString:表示由Rust分配並且可以傳遞給C語言的字符串
  • OsStr:表示和操作系統相關的字符串,主要為了兼容Windows
  • OsString:OsStr的可變版本
  • Path:表示路徑
  • PathBuf:是Path的可變版本

本文我們重點討論前兩種,因為它們是開發過程中最常用的,也是比較容易混淆的。對於str,我們常見的是它的引用類型,&str。如果你看過了 一文後,相信你已經瞭解了引用類型和Ownership的概念。也就是說String類型具有Ownership而&str沒有。

在Rust中,String本質上是Vec,Vec是向量集合的關鍵字,我們在後面會介紹。String類型由三個部分組成,分別是:指向堆中字節序列的指針,記錄堆中字節序列的長度和堆分配的容量。通過一段代碼也許你很有更深的理解。

<code>fn main() {
let mut a = String::from("foo");
println!("{:p}", a.as_ptr());
println!("{:p}", &a);
assert_eq!(a.len(), 3);
a.reserve(10);
assert_eq!(a.capacity(), 13);
}
複製代碼/<code>

在這段代碼中我們可以看到,a.as_ptr()獲取指針和&a獲取的指針是不一樣的。


Rust入坑指南:鱗次櫛比


這裡我們解釋一下,as_ptr獲取到的指針是堆中字節序列的指針地址,而&a的地址是字符串變量在棧上的指針地址。另外,len()和capacity()方法得到的長度都是字節數量,而非字符數量。這裡你可以自己動手試試中文字符的長度。

聊完了字符串的基本概念以後,相信你已經對Rust的字符串有了一個大概的認識。接下來我們就一起來看一看字符串的CRUD的方法吧。

創建字符串

話不多說,先來展示一下創建字符串的各種方法。

<code>fn main() {
let string: String = String::new();
let string: String = String::from("hello rust");
let string: String = String::with_capacity(10);
let str: &'static str = "Jackey";
let string: String = str.to_owned();
let string: String = str.to_string();
}
複製代碼/<code>

我們比較常用的是前兩種,下面介紹一下後面幾個方法。with_capacity()是創建一個空字符串,參數表示在堆中分配的字節數。to_owned和to_string是演示瞭如何把&str類型轉換成String類型。

修改字符串

Rust修改字符串的常用方法也有很多,例如在字符串後追加,連接兩個字符串,更新字符串等。下面這段代碼就展示了一些修改字符串的方法。

<code>fn main() {
let mut hello = String::from("Hello, ");
hello.push('J'); // 追加單個字符
hello.push_str("ackey! "); //追加字符串
println!("push: {}", hello);

hello.extend(['M', 'y', ' '].iter()); //追加多個字符,參數為迭代器
hello.extend("name".chars());
println!("extend: {}", hello);

hello.insert(0, 'h'); //類似於push,可以指定插入的位置
hello.insert(1, 'a');
hello.insert(2, '!');
hello.insert_str(0, "Haha");
println!("insert: {}", hello);

let left = "Hello, ".to_string();
let right = "World".to_string();
let result = left + &right;
println!("+: {}", result); //使用+連接字符串時,第二個必須為引用
let mut message = "rust".to_string(); //使用+=連接字符串時,字符串必須定義為可變
message += "!";
println!("+=: {}", message);

let s = String::from("foobar");
let s: String = s
.chars()
.enumerate()
.map(|(_i, c)| {c.to_uppercase().to_string()})
.collect();
println!("update chars: {}", s);

let s1 = String::from("hello");
let s2 = String::from("rust");

let s3 = format!("{}-{}", s1, s2);
println!("format: {}", s3);
}
複製代碼/<code>

我們對上面的代碼做一些補充的解釋。

push和insert類似,帶有_str的方法接收的參數是字符串,否則只能接收單個字符。insert可以指定插入的位置,而push只能在字符串末尾插入。

使用「+」連接字符串時,第一個參數是String類型,第二個則需要是引用類型&str。這類似於我們調用一個add方法,它的定義是這樣的:

<code>fn add(self, s: &str) -> String {
複製代碼/<code>

所以,第一個參數的ownership轉移到了函數中,又通過返回結果傳遞出來。也就是說,在使用了+操作符之後,left已經沒有ownership了。

字符串查找

在Rust中,字符串是不能根據位置來獲取到指定字符的。也就是下面這段代碼是編譯不過的。

<code>let s1 = String::from("hello");
let h = s1[0];
複製代碼/<code>

因為,Rust會認為這個0是指第一個字節,而Rust字符串中的字符可能佔有多個字節(還記得前面我讓你用中文字符實驗代碼嗎?)所以,如果你單純的想要獲取一個字節,編譯器不知道你是真的想要獲取字節對應的數值,還是要獲取那個字符。

我們在處理字符串時通常有以下方法:

<code>fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
println!("{}", s);

let chars = hello.chars();
for c in chars {
println!("{}", c);
}

let bytes = hello.bytes();
for byte in bytes {
println!("{}", byte);
}

let get = hello.get(0..1);
let mut s = String::from("hello");
let get_mut = s.get_mut(3..5);

let message = String::from("hello-world");
let (left, right) = message.split_at(6);
println!("left: {}, right: {}", left, right);
}
複製代碼/<code>

通常是使用字符切片,也可以使用chars方法獲取到Chars迭代器,然後可以對每個字符進行單獨處理。此外,使用get或get_mut方法也可以接收索引範圍,返回指定的字符串切片。返回結果是Option類型,這是因為如果指定的索引返回不能返回完整字符,那麼Rust就會返回None。這裡也可以使用is_char_boundary方法來判斷一個位置是否是非法邊界。

最後,也可以使用split_at或split_at_mut方法來分割字符串。這要求分割的位置正好是字符邊界位置,如果不是,程序就會崩潰。

刪除字符串

Rust的標準庫提供了一些刪除字符串的方法,我們來演示一些:

<code>fn main() {
let mut hello = String::from("hello");
hello.remove(3);
println!("remove: {}", hello);
hello.pop();
println!("pop: {}", hello);
hello.truncate(1);
println!("truncate: {}", hello);
hello.clear();
println!("clear: {}", hello);
}
複製代碼/<code>

結果如圖:


Rust入坑指南:鱗次櫛比


remove方法用來刪除字符串中的某個字符,其接收的參數是字符的起始位置,如果是不是某個字符的起始位置,會導致程序崩潰。

pop方法會彈出字符串末尾的字符,truncate方法是截取指定長度字符串,而clear方法則是用來清空字符串。

至此,關於Rust中的字符串的基本概念和CRUD我們都已經介紹完了,接下來我們再來看另一種集合類型Vector。

Vector

Vector是用來存儲相同數據類型的多個數據一種數據類型。它的關鍵字是Vec。下面我們一起來看看向量的CRUD吧。

創建向量

<code>fn main() {
let v1: Vec = Vec::new();
let v2 = vec![1, 2, 3];
}
複製代碼
/<code>

上面這段代碼演示了創建一個向量的兩種方式,第一種是使用new函數來創建一個空的向量,由於沒有添加元素,所以要顯式的指定存儲元素的類型。第二種是創建一個有初始值的向量集合,我們直接使用vec!宏,然後指定初始值即可,不需要指定向量中元素的數據類型,因為編譯器可以自己推斷出來。

更新向量

<code>fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
}
複製代碼/<code>

創建一個空的向量之後,如果我們想要增加元素,就可以直接使用push方法,向末尾追加元素。

刪除向量

<code>fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);

v.pop();
for i in &v {
println!("{}", i);
}
}
複製代碼/<code>

刪除單個元素可以使用pop方法,而要刪除整個向量,只能像其他結構體一樣,到其ownership失效。

讀取向量元素

<code>fn main() {
let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
println!("The third element is {}", third);

match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),

}

let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
}
複製代碼/<code>

當你需要讀取單個指定元素時,有兩種方法可以用,一種是使用[],另一種是使用get方法。兩種方法的區別是:第一種返回的是元素的類型,而get返回的是Option類型。如果你指定的位置越界了,那麼使用第一種方法程序會直接崩潰,而使用第二種方法則會返回None。

此外,還可以通過遍歷向量的形式來讀取元素。如果想要存儲不同類型的數據,我們可以藉助枚舉類型。

<code>fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
複製代碼/<code>

HashMap

HashMap存儲了KV結構的數據,各個Key必須是同一種類型,各個Value必須是同一種類型。由於HashMap是三種集合類型中使用最少的,所以在使用之前,需要手動引入進來

<code>use std::collections::HashMap;
複製代碼/<code>

創建HashMap

首先我們來了解一下如何創建一個新的Hash Map並增加元素。

<code>use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);
}
複製代碼/<code>

注意,在使用insert方法時,field_name和field_value都會失去所有權。那如何再使用它們呢?我們只能從Hash Map中再拿出來。

訪問Hash Map的數據

<code>use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);

let favorite = String::from("Favorite color");
let color = map.get(&favorite);
match color {
Some(x) => println!("{}", x),
None => println!("None"),
}
}
複製代碼/<code>

可以看到,我們使用get可以獲取到指定Key的值,get方法返回的是Option類型,如果沒有指定的Value,則會返回None。此外,也可以使用for循環來遍歷Hash Map。

<code>use std::collections::HashMap;
fn main() {
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);
}
}
複製代碼/<code>

更新Hash Map

當我們向同一個Key insert值時,舊的值就會被覆蓋。如果只想要在Key不存在時插入,則可以使用entry。

<code>use std::collections::HashMap;
fn main() {
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);
}
複製代碼/<code>

總結

今天帶大家一起挖了三個坑,string,vector和hash map,分別介紹了每種數據類型的CRUD。對string的介紹佔了比較大的篇幅,因為它是最常用的數據類型之一。當然這部分的相關知識還有很多,歡迎大家和我一起學習交流。


分享到:


相關文章: