Rust语言中的String和HashMap使用示例详解
作者:SaraiNoQ
String
字符串是比很多开发者所理解的更为复杂的数据结构。加上 UTF-8 的不定长编码等原因,Rust 中的字符串并不如其它语言中那么好理解。
Rust 的核心语言中只有一种字符串类型:str
。字符串 slice,它通常以被借用的形式出现:&str
是一些储存在别处的 UTF-8 编码字符串数据的引用。而 String
的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。
💡 Rust 标准库中还包含一系列其他字符串类型,比如 OsString
、OsStr
、CString
和 CStr
。相关库 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);
💡 Entry
的 or_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
循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。
总结
掌握了 Rust 中最常用的三种集合类型,现在你已经可以开始进行一些包含复杂逻辑的编程了!在此过程中你可能会遇到很多错误。因此接下来我将介绍错误处理与模式匹配,并开始介绍一些测试工具和自动化测试的内容,更多关于Rust String HashMap使用的资料请关注脚本之家其它相关文章!