Rust语言中的String和HashMap使用示例详解

Rust
391
0
0
2023-06-02
目录
  • String
  • 新建字符串
  • 更新字符串
  • 使用 + 运算符或 format! 宏拼接字符串
  • 索引字符串
  • 字符串 slice
  • 遍历字符串
  • HashMap
  • 新建 HashMap
  • HashMap 和 ownership
  • 访问 HashMap 中的值
  • 更新 HashMap
  • 直接覆盖
  • 新插入
  • 更新旧值
  • 总结

String

字符串是比很多开发者所理解的更为复杂的数据结构。加上 UTF-8 的不定长编码等原因,Rust 中的字符串并不如其它语言中那么好理解。

Rust 的核心语言中只有一种字符串类型:str。字符串 slice,它通常以被借用的形式出现:&str 是一些储存在别处的 UTF-8 编码字符串数据的引用。而 String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。

💡 Rust 标准库中还包含一系列其他字符串类型,比如 OsStringOsStrCStringCStr。相关库 crate 还会提供更多储存字符串数据的数据类型。这些字符串类型能够以不同的编码,或者内存表现形式上以不同的形式,来存储文本内容。

新建字符串

  • String::new()函数
  • to_string()方法
let data = "initial contents";
let s = data.to_string();
// 或者
let s = String::from(data);
// 该方法也可直接用于字符串字面量:
let s = "initial contents".to_string();

更新字符串

String 的大小可以增加,其内容也可以改变。另外,还可以使用 + 运算符或 format! 宏来拼接 String 值。

  • push_str()
  • push()
let mut s = String::from("foo");
let t = String::from("bar");
s.push_str(&t);
// push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中
let mut l = String::from("lo");
l.push('l');

💡 pub fn push_str(&mut self, string: &str) 方法不会获得字符串的所有权。另外值得一提的是,t&String 类型,而 push_str 方法需要的是 &str 类型的参数。为什么这段代码能够正常编译呢?这里就涉及到了 解引用强制转换(deref coercion),我们将在后面的文章中介绍它。

使用 + 运算符或 format! 宏拼接字符串

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;

你可以把他理解成 C++ 的运算符重载。在 Rust 中, + 的实现可能是 fn add(self, s: &str) -> String 这样一个方法。

💡 s1 的所有权将被移动到 add 调用中

如果想要级联多个字符串,使用 + 就变得麻烦了。这时候可以使用 format! 宏:

let s1 = String::from("hello");
let s2 = String::from("the");
let s3 = String::from("world");
let s = format!("{}-{}-{}", s1, s2, s3);

索引字符串

在其他语言中,通过索引来引用字符串中的某个单独字符是很常见的操作。但在 Rust 中,你可能会遇到问题:

这主要是因为:

  • UTF-8 是不定长编码,而 String 的实现是基于 Vec<u8> 的封装:数组中每一个元素都是一个字节,但 UTF-8 中每一个汉字(或字符)都可能由一到四个字节组成
  • 索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。

字符串 slice

如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你明确字符串范围。这时你需要一个字符串 slice,使用 [] 和一个 range 来创建含特定字节的字符串 slice:

fn main() {
    let s1 = String::from("你好,");
    println!("{}", &s1[0..3]);  // 你
}

如果获取 &s1[0..1] ,Rust 在运行时会 panic。因此,你应该谨慎地使用这个操作,因为这么做可能会使你的程序崩溃。

遍历字符串

可以使用 chars() 方法获取该字符串的字母数组。

fn main() {
    let s1 = String::from("你好,");
    let s2 = String::from("世界!");
    let s3 = s1 + &s2; 
    for char in s3.chars() {
        println!("{}", char);
    }
}

HashMap

另外一个常用集合类型是 哈希 map(hash map)。HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。

哈希 map 适用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。

新建 HashMap

使用new 创建一个空的 HashMap,并使用 insert 增加元素:

use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

💡 必须首先 use 标准库中集合部分的 HashMap。在这上面介绍的三个常用集合中,HashMap 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap 的支持也相对较少,例如,并没有内建的构建宏。

💡 像 vector 一样,哈希 map 将它们的数据储存在堆上;哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

另一个构建哈希 map 的方法是使用一个元组的 vector 的 collect 方法:

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();

HashMap 和 ownership

  • 对于像 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。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。此时就涉及到生命周期的内容。

访问 HashMap 中的值

可以通过 get 方法并提供对应的键来从哈希 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);

get 返回 Option<V>,所以结果被装进 Some;如果某个键在哈希 map 中没有对应的值,get 会返回 None。当获取到结果后,就需要使用到 match 进行匹配。

更新 HashMap

在更新前,我们需要考虑以下几种情况:

  • 已有 key-value,直接覆盖
  • 只在没有 key-value插入
  • 利用已有 key-value更新

直接覆盖

insert() 方法:

use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);

新插入

利用 entry() 函数返回的枚举值,调用 or_insert() 方法进行处理:

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);

💡 Entryor_insert 方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。

更新旧值

or_insert 方法事实上会返回这个键的值的一个可变引用(&mut V):

// 统计字符串中某个单词的出现次数
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); // 之前不存在对应关系就初始化并置计数器为0
    *count += 1; // 每次计数器加一
}
println!("{:?}", map);

💡 这里我们将这个可变引用储存在 count 变量中,所以为了赋值必须首先使用星号( * )解引用 count。这个可变引用在 for 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。