8.1-2 Vector

Vector是堆内存的数据结构,使用Vec<T>定义 (类似C++的STL中的vector

  • 由标准库提供
  • 可存储多个值
  • 只能存储相同类型的数据
  • 值在内存中连续存放

初始化

例子1

1
let v: Vec<i32> = Vec::new();

事实上,这里不指定v的类型也可以,当Vector插入第一个元素的时候,编译器会根据该元素推断

例子2

在使用一些指定初始值创建Vector时,可以使用vec!这个宏,如

1
let v = vec![1, 2, 3];

Push元素

与C++的std::vector类似:

1
2
3
v.push(1);
v.push(2);
v.push(3);

删除Vector

与struct等类似,当其离开作用域,将会连同内部元素被一同清理

读取Vector的元素

有两种方式可以读取Vector的元素,索引和get方法,例如

1
2
3
4
5
6
7
8
let v: Vec<i32> = vec![1, 2, 3];
let third = &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"),
}

在处理越界问题时,

  • 索引会使程序panic并终止
  • get则返回None

这方便我们根据需求选择读取方式

所有权和借用规则

前面提到过的规则对于Vector也适用,例如

1
2
3
4
5
6
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3];
let third = &v[2]; // immutable borrow
v.push(4); // mutable borrow
println!("The third element is {}", third); // immutable borrow
}

会报错,因为不能同时出现可变借用和不可变借用

  • 其中push(&mut self, value: T)函数是定义在可变借用上

在底层原理上,这么设计的原因是:

  • 假如现在需要向某个Vector添加元素,但是原始的内存块长度不够了,需要在另一个地方重新寻找内存块来创建新Vector,这就会导致不可变借用依然指向旧的地址,从而出现错误

遍历Vector

对于不可变引用:

1
2
3
4
5
6
fn main() {
let mut v: Vec<i32> = vec![3, 2, 4, 5, 1];
for i in &v {
println!("{}", i);
}
}

对于可变引用(例如修改数组元素):

1
2
3
4
5
6
7
fn main() {
let mut v: Vec<i32> = vec![3, 2, 4, 5, 1];
for i in &mut v {
*i += 50;
println!("{}", i);
}
}
  • 其中*是解引用运算符

Vector存储“多种数据”

前面说过,Vector不能直接存储多种数据,但是可以借助enum来实现,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

fn main() {
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}

8.3-4 String

字符串本质上是Byte的集合,并提供一些方法将Byte解析成文本

在Rust的核心语言层面,只有一个字符串类型:

  • 字符串切片类型str&str

所谓的String类型则来自标准库而非核心语言

  • 可增长、可修改、可拥有

在Rust中常说的字符串指的是String&str,它们都是UTF8编码

此外,Rust标准库还提供了其它的字符串类型,例如OsString, OsStr, CString, CStr等等

  • 一般以String结尾的字符串是可获得所有权的
  • 一般以Str结尾的字符串是可借用的

创建一个字符串 (String)

创建空字符串:

1
let mut s = String::new();

从字面值创建一个字符串:

1
2
3
4
let data = "initial contents";
let s = data.to_string();
let s1 = "initial contents".to_string();
let s2 = String::from("initial contents");
  • 其中to_string()方法可以用于实现了Display trait的类型,包括字符串字面值

更新String

可以使用push_str()方法来拼接

1
2
3
let mut s = String::from("foo");
s.push_str("bar");
println!("{}", s);

其中push_str(&mut self, string: &str)不会获得字符串的所有权,例如我们也可以

1
2
3
4
let mut s = String::from("foo");
let s1 = String::from("bar");
s.push_str(&s1);
println!("{}", s);

如果想增加增添字符,可以适用push(),例如

1
2
3
let mut s = String::from("lo");
s.push('l');
println!("{}",s);

拼接的加法运算符

对于String,使用+连接字符串,会使用类似fn add(self, s: &str) -> String {...}的方法,例如

1
2
3
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2;
  • 其中&s2是对String类型的引用,而s1在拼接操作之后将不可用,因为add函数会取得第一个参数的所有权
  • 此外,虽然&s2不是&str类型,但标准库的add方法使用了泛型,对其进行了解引用强制转换(deref coercion)

拼接的宏

使用format!这个宏,可以连接多个字符串,例如

1
2
3
4
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s3 = s1 + "-" + &s2 + "-" + &s3;

可以改写为

1
let s = format!("{}-{}-{}", s1, s2, s3);
  • 相比+format!不会取得参数的所有权,因此这些参数之后可以继续使用

对String以索引形式访问

如果类似Vector那样访问String元素,例如

1
2
let s1 = String::from("hello");
let h = s1[0];

则会报错,因为String并没有实现index索引这个trait!

  • 这是因为String的底层是Vec<u8>的包装,而对于不同的语言,每个字符会对应不同的长度,从而引起问题,因此Rust便在设计上杜绝了这种使用方式

Bytes, Scalar values, Grapheme clusters

从字节角度来看待字符串:

1
2
3
4
let w = "東京駅";
for b in w.bytes() {
println!("{}", b);
}

输出为

1
2
3
4
5
6
7
8
9
230
157
177
228
186
172
233
167
133

从标量值来看待字符串:

1
2
3
4
let w = "東京駅";
for b in w.chars() {
println!("{}", b);
}

输出为

1
2
3



对于最后一种Grapheme clusters的方法,需要借助第三方crate来实现,这里省略

  • 还有一个Rust不允许String进行索引的原因是
    • 索引操作应当消耗O(1)时间
    • 但String需要遍历所有内容来查看有多少合法字符,因此无法保证O(1)

切片String

不同语言的字符所占的字节不同,例如每个汉字占3个字节,因此

1
2
3
let w = "東京駅";
let s = &w[0..3];
println!("{}", s);

将会拿到这个字,若少于3个字节则会报错

对于英文,则没有这个问题:

1
2
let w = "abcdefg";
let s = &w[0..3];

因此,在切割字符串时,必须注意字符边界,否则程序会panick

8.5 HashMap

即哈希查找或哈希表,对于HashMap<K, V>

  • K为key,即键
  • V为value,即值

本部分暂略,回头补