序
前面的学习中,我们已经学习了基础类型,枚举类型,与结构体,本期我们来讲一下,String类型,动态数组 Vector 与HashMap。
动态数组 Vector
前面我们提到过数组类型,并且说了他是定并且类型必须一致,这样的数组明显不能满足我们的开发需求。
vector
允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。rust提供了新的数组类型动态数组 Vector
。
使用用 Vec::new
函数,我们可以轻松创建一个Vector。
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,rust编译器不知道他是什么类型的,但是不用担心,rust可以根据上下文推断。后续使用就明白了。
fn main() {
let v: Vec<i32> = Vec::new();
}
另一种创建的方式,使用vec!
宏,这个宏会根据我们提供的值来创建一个新的 vector。
fn main() {
let v = vec![1, 2, 3];
}
这里提供了 i32
类型的初始值,Rust 可以推断出 v
的类型是 Vec<i32>
,因此类型注解就不是必须的。
更新 vector
更新一个vector 和JavaScript也一样提供了push
方法,别忘了添加mut
。
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
读取 vector 的元素
rust提供了两种方法引用 vector 中储存的值:通过索引或使用 get
方法。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
这两种方式主要的区别就是:
- 索引有可能会发生
索引溢出
,会发生panic
get
方式不会,它不会panic
而是返回None
panic
代表出现发生错误,我们可以手动调用panic
宏,类似于typescript中throw Error
遍历 vector 中的元素
我们可以使用前面提到for in
来遍历vector,
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
我们也可以获取一个可变引用,改变其中的值
注意这里我们用到了
*
,这代表解引用
刚好遇&
相反
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
使用枚举来储存多种类型
刚才我们说到Vector也只可以储存同一种类型的值,但是我们可以配合枚举类型,达到储存多种类型值的目的。
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),
];
}
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。
如果Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。这对于安全要求高的语言来说是不可以接受的。
使用枚举外加 match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况,
当然在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,后面会讲到。
标准库中 Vec
定义的很多其他实用方法的 API 文档。例如:pop,len 等
丢弃 vector 时也会丢弃其所有元素
类似于任何其他的 struct
,vector 在其离开作用域时会被释放,
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。借用检查器确保了任何 vector 中内容的引用仅在 vector 本身有效时才可用。
String
字符串在rust中对于JavaScript重度使用者来说,确实有些反人类,不方便了,可能这是rust对于安全性考量,做的一些妥协吧。
字符串是比很多程序员所想象的要更为复杂的数据结构,
什么是字符串?
Rust 的核心语言中只有一种字符串类型:字符串 slice str
,它通常以被借用的形式出现,&str
。
新建字符串
很多 Vec
可用的操作在 String
中同样可用,事实上 String
被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。这也是我们为什么先讲vector的原因.
使用String::from
我们可以轻松的创建一个字符串,并且下面的字符串都是合法的,rust字符串支持utf-8
.
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
使用 to_string
方法,它能用于任何实现了 Display
trait 的类型,比如字符串字面值。
fn main() {
let data = "initial contents";
let s = data.to_string();
// 该方法也可直接用于字符串字面值:
let s = "initial contents".to_string();
}
更新字符串
String
的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 Vec
的内容一样。另外,可以方便的使用 +
运算符或 format!
宏来拼接 String
值。
使用 push_str 和 push 附加字符串
可以通过 push_str
方法来附加字符串 slice,从而使 String
变长,注意这里所有权的转移。
push_str
拼接一个字符串字面量&str
,push
拼接单个char
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}
使用 + 运算符或 format! 宏拼接字符串
通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用 +
运算符,
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
}
这里需要注意的是,+
会得到左边字符串的所有权,右边是引用,因为这里相当于内部实现了add方法 fn add(self, s: &str) -> String {...}
在标准库中你会发现,add
的定义使用了泛型和关联类型。在这里我们替换为了具体类型,这也正是当使用 String
值调用这个方法会发生的。
如果想要多个字符串,+
的行为就想一个憨憨一样了
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
}
rust提供了 format!
宏
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
}
是不是感觉rust,遇到处理困难的语法,就会使用宏
,面向宏编程
。
索引字符串
在typescript中,我们可以通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 String
的一部分,会出现一个错误。
fn main() {
let s1 = String::from("hello");
let h = s1[0];
}
程序会发生错误,Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?
内部表现
刚才我们说了String
是一个 Vec<u8>
的封装。并且支持utf-8
,所以一个我们所理解的字符实际上在内部实现上并不是占一个字节。索引访问到的值可能是一个毫无意义的值。
最后一个 Rust 不允许使用索引获取 String
字符的原因是,索引操作预期总是需要常数时间(O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字符串 slice
索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用 []
和单个值的索引,可以使用 []
和一个 range 来创建含特定字节的字符串 slice:
#![allow(unused)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
}
这里,s
会是一个 &str
,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 s
将会是 “Зд”。
如果获取 &hello[0..1]
会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样
你应该小心谨慎地使用这个操作,因为这么做可能会使你的程序崩溃。
遍历字符串的方法
操作字符串每一部分的最好的方法是明确表示需要字符还是字节。
#![allow(unused)]
fn main() {
for c in "Зд".chars() {
println!("{c}");
}
for b in "Зд".bytes() {
println!("{b}");
}
}
-
chars
方法会将其分开并返回两个char
类型的值,接着就可以遍历其结果来访问每一个元素了: -
另外
bytes
方法返回每一个原始字节,这可能会适合你的使用场景
从字符串中获取如同天城文这样的字形簇
是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。
字形簇
就是我们视觉上的一个字符
字符串并不简单
字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 String
数据作为所有 Rust 程序的默认行为。
现在你知道JavaScript为我们负重前行
了吧😂
rust标准库还是提供了很多围绕 String
和 &str
构建的功能,来帮助我们正确处理这些复杂场景。例如 contains
来搜索一个字符串,和 replace
将字符串的一部分替换为另一个字符串。并不是完全不考虑开发人员。
HashMap
最后我们讲解一下HashMap,HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射。
新建一个哈希 map
可以使用 new
创建一个空的 HashMap
,并使用 insert
增加元素。注意必须首先 use
标准库中集合部分的 HashMap
。在这三个常用集合中,HashMap
是最不常用的,所以并没有被 prelude 自动引用。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 HashMap
的键类型是 String
而值类型是 i32
。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
访问哈希 map 中的值
可以通过 get
方法并提供对应的键来从哈希 map 中获取值,
fn main() {
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).copied().unwrap_or(0);
}
哈希 map 和所有权
对于像 i32
这样的实现了 Copy
trait 的类型,其值可以拷贝进哈希 map。对于像 String
这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者,
fn main() {
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 不再有效,
// 尝试使用它们看看会出现什么编译错误!
}
当 insert
调用将 field_name
和 field_value
移动到哈希 map 中后,将不能使用这两个绑定。
如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。
更新哈希 map
当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值。或者可以结合新旧两值。
覆盖一个值
连续插入相同key的值会覆盖前一个
fn main() {
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
方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。
fn main() {
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);
}
根据旧值更新一个值
另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。
fn main() {
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);
*count += 1;
}
println!("{:?}", map);
}
split_whitespace
方法返回一个由空格分隔 text
值子 slice 的迭代器。or_insert
方法返回这个键的值的一个可变引用(&mut V
)。这里我们将这个可变引用储存在 count
变量中,所以为了赋值必须首先使用星号(*
)解引用 count
。这个可变引用在 for
循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。
你只需要记住这个玩意和我们JavaScript中的Map
特性差不多,区别
- rust hashMap 是同构的,就是仅允许,key的类型相同,value的类型也要相同
- 提供的方法不一样,JavaScript提供了
get
、set
,clear
等,rust提供了get
、insert
等。
哈希函数
HashMap
默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)1 的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了BuildHasher
trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
结语
这一期我们又学习三种复杂类型,至此rust的基础已经差不多快完了,下一期我们讲到错误处理,刚才我们也提过panic!
宏。
rust学习之旅至此才刚刚开始。😂
坐稳准备发车了。🤔