RUST 0x06 Common Collections

RUST 0x06 Common Collections

1 Vector

vector,Vec<T>,能存储相同类型的值。

创建一个Vector

要创建一个空的vector,可以调用Vec::new函数,如:

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

注意,这里写出类型名Vec<i32>是因为我们没有在这个vector中插入任何值,所以我们就需要让Rust知道我们想要存储什么类型的值。

如果我们在创建vector时就已经插入了值,那么就没有必要再注明类型名了——Rust可以从插入的值推断类型。

如果要创建含初始值的vector,可以使用Rust中的vec!宏,如:

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

因为我们已经给了i32的初始值,所以Rust可以推断出来v的类型是Vec<i32>

更新一个Vector

如果想在vector后面添加元素,我们可以用pushmethod,如:

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

就像对所有变量一样,如果我们想要改变vector的值,我们需要用mut来让其可变。

此外,因为在后面的push操作中我们往vector中插入了i32,所以Rust可以联系上下文推断出v的类型是Vec<i32>,因此我们也无需特别注明。

Dropping a Vector Drops Its Elements

struct一样,vector脱离作用域时,其元素所用的内存也会被释放。

读取Vector的元素

有两种方法:直接用下标或者用getmethod,如:

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."),
}

有两个要注意的地方:

  1. 我们使用index2来获取第三个元素。(从0开始数)
  2. 两种方法得到的都是引用类型。

此外,getmethod的返回值是Some(&element)None,也就是说,就算传递给它一个超越范围的index,也能通过编译并正常运行。

ところで、就像之前(https://zhuanlan.zhihu.com/p/86987925)所说,如下代码将会抛出一个CE:

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);

错误信息为:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {}", first);
  |                                          ----- immutable borrow later used here

为什么对第一个元素的引用要关心vector的end的改变呢?这是因为vector的工作机制:加入一个新元素到vector尾时,如果此时已经分配到内存不足够用来加入新元素,则需要分配更多内存,这时vector就会索取一段新的内存,同时复制所有旧的元素到那个新的内存地址,所以旧的内存地址就会被废弃,此时对第一个元素的引用就会指向一段被废弃的地址,而这是不被Rust的内存保护机制允许的。

迭代遍历Vector的值

使用for就可以实现这样的效果。且有两种方法。

第一种是获得不可变的引用:

let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}

第二种是获得可变的引用:

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

为了改变可变引用指向的元素,我们需要使用dereference operator(*)来获得i指向的值。

(所以,使用println!("{}", *i);也能实现与第一种相同的效果)

运用Enum来存储多种类型

在开始时我们就说过了,vector只能存储相同类型的值。但是如果我们想要存储多种类型的值时怎么办呢?答案就是运用Enum。如:

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

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

pop

popmethod可以移除vector的最后一个元素并且返回它,如:

println!("Pop last element: {:?}", xs.pop());

2 String

什么是String?

Rust的核心语言中只有一种string type——字符串切片str

而Rust的standard library提供了String类型,

当我们谈到“strings”时,我们其实是同时指代了String&str。而且这两者都是UTF-8编码的。

Rust的standard library里还包括了一些其他的string type,比如OsStringOsStrCStringCStr。这些string type可以以不同的编码与在内存中的存储方式来存储文本。

创建一个String

很多对Vec<T>能进行的操作也能对String进行,比如new

let mut s = String::new();

此外,也可以用to_stringmethod来创建一个有初始值的String

let data = "initial contents";
let s = data.to_string();
let s = "initial contents".to_string();

当然,也可以用之前用过很多次的String::from

let s = String::from("initial contents");

更新一个String

String可以变长也可以改变内容,就像Vec<T>一样,可以"push"数据在它尾部。此外,也可以选择更加方便的+运算符或者format!宏来拼接String值。

push_strpush来append

我们可以用push_strmethod来在String后拼接上一个字符串切片,如:

let mut s = String::from("foo");
s.push_str("bar");

push_strmethod的参数类型是一个字符串切片,因为我们不一定想要让它夺走ownership。

+运算符或format!宏来拼接

如果想要组合两个已经存在的String,可以使用+运算符:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1被move了,于是不能再被使用

之所以s1在进行+之后不再有效与s2之前要加上引用符号,是因为当我们调用+运算符时,它会使用addmethod,像这样:

fn add(self, s: &str) -> String {

(这并不是add在standard library里准确的亚子)

add函数里,我们只能在String后加上一个&str。但为什么我们在应该接受&str的地方传递了一个&String却仍然能通过编译呢?

那是因为Rust的编译器可以使&String强制类型转换为&str,使&s2变成了&s2[..]

因为add函数没有夺走s参数的ownership,所以在进行运算后,s2仍然有效。

如果我们想要拼接多个字符串,用+就会显得很冗杂:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;

不仅要输入一堆+",而且很难看清这行代码到底想做什么。

所以为了对付更加复杂的字符串组合,我们可以使用format!宏:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);

format!宏就像println!一样运作,只不过它是返回一个String而不是在屏幕上打印输出。

Indexing into Strings

在很多其他语言里,我们可以像这样来获取字符串中的某个字符:

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

但是如果在Rust中这样写,就会获得一个CE。Rust的字符串不支持indexing。为什么?为了回答这个问题,我们需要讨论一下Rust怎么在内存中存储字符串。

Internal Representation

String就相当于一个Vec<u8>,让我们看些UTF-8编码的例子:

let len = String::from("Hola").len();

在这个情况,len是4,因为这个vector存储的“Hola”有4个字节长,每个字符占据了一个字节。

let len = String::from("Здравствуйте").len();

但是这个字符串的len并非12,而是24:这是在UTF-8编码下 “Здравствуйте” 所占据的字节数,因为在这个字符串里的每个Unicode单值占据了内存的2个字节。因此,index就并不总是与一个Unicode单值相关,比如:

let hello = "Здравствуйте";
let answer = &hello[0];

当我们用UTF-8编码编码这个字符串时, З 的第一个字节是208,第二个字节是151,所以answer实际上会相当于208,但是208本身并非一个有效字符。为了避免返回一个不想要的值从而造成我们不一定会很快发现的bug,Rust直接采取了不编译这段代码。

Bytes & Scalar Values & Grapheme Clusters

从Rust的视角来看待字符串有三种方法:bytes, scalar values, and grapheme clusters(离我们称作“字符”最接近的东西)。

让我们康康 “नमस्ते” 在Vec<u8>中的存储方式:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

这总共是18字节,也正是计算机最终存储这个数据所采取的方式。

如果从Unicode scalar value,也就是Rust中的char类型来看它,它就是:

['न', 'म', 'स', '्', 'त', 'े']

这总共是6个char值,但是第四个和第六个其实并不是字母:它们是附加符号(diacritics),本身并没有意义。

如果从grapheme clusters来看,它就是印地语中的四个字母:

["न", "म", "स्", "ते"]

Rust能提供不同的表示字符串数据的方法,所以每个程序可以选择它所需要的那种表示方法,无论数据里存的是哪种语言。

另外一个Rust不允许我们index to a String的原因是,index操作的时间复杂度被希望是(O(1)),但是在String中并不能保证这样,因为Rust需要从头遍历字符串的内容来确认里面有多少个有效字符。

Slicing Strings

let hello = "Здравствуйте";
let s = &hello[0..4];

在上面的代码中,s会是一个包含着4个字节的&str,又因为这里每个字符都是2字节,所以sЗд

但是如果我们写了&hello[0..1],则在运行中就会抛出一个RE:

thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4

所以在玩字符串切片时要小心,因为它可能会crash恁的程序。

遍历字符串的方法

如果想要对单个Unicode scalar value做操作,最好的方法是使用charsmethod。如:

for c in "नमस्ते".chars() {
    println!("{}", c);
}

它会输出:

न
म
स
्
त
े

bytesmethod会返回每一个raw byte,如:

for b in "नमस्ते".bytes() {
    println!("{}", b);
}

它会输出组成这个String的那18个字节。

而standard library并不提供得到grapheme clusters的方法,可以去crates.io找到相应的crate。

3 Hash Maps

类型HashMap<K, V>存储着一个从类型K的Key到类型V的Value的映射。

创建一个Hash Map

可以用new来创建一个空的hash map,并且可以用insert来加入元素。如:

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

在三种common collections里,hash map是使用频率最低的,所以它并不会自动地被包含在域里,所以我们需要首先使用use来从std::collections中引入它。

就像vector一样,hash map将数据存储在堆上。例子中的HashMap有类型为String的Key和类型为i32的Value。就像vector一样,hash map中的所有key都应该是同一类型的,所有value也应该是同一类型的。

另外一种创建hash map的方法就是在一个vector of tuples上使用collectmethod。如果我们有两个分别存储key和value的vector,我们可以使用zipmethod来创建一个vector of tuples使它们两两对应地匹配,然后就可以使用collect方法使其变为一个hash map。如:

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<_, _>因为collect可以生成很多collection类型,所以如果不指出我们需要的是hash map,Rust就不知道该返回哪个。在key和value的类型参数上我们用了_,因为Rust可以从collect的vector中推断出hash map中要包含的类型。

Hash Maps & Ownership

对可Copy的类型,比如i32,只有它的值被复制到hash map里。但是对owned values比如String,它的值会被move,从而hash map会变成这些值的owner,如:

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不再有效

如果想要在生成hash map之后继续使用这些值,我们可以考虑使用它的引用来生成hash map。

获取Hash Map中的值

我们可以用getmethod来用key获取hash 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会返回Some(&10),返回值被包在Some里是因为get的返回值是Option<&V>,如果hash map中没有该key对应的value,则会返回一个None。因此我们还需要处理返回的Option

我们也可以用类似我们迭代遍历vector所采取的方法:

use std::collections::HashMap;

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

更新一个Hash Map

Overwriting a Value

如果我们想要插入一对key和value,hash map中又已经存在了那个key,并对应着不同的value,那么那个原来的value会被新的value给替换。

Only Inserting a Value If the Key Has No Value

hash map有一个特殊的API叫做entry,它可以用来检测恁想要插入的key在hash map中存不存在并返回一个enum,如:

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_insertmethod,如果那个key在hash map中已经存在,则会返回对应的value的一个可变引用,如果不存在,则插入它的参数作为对应的value。

Updating a Value Based on the Old Value

另外一种常见的情况是查找一个key对应的value,然后依据这个value更新成新的value。如:

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

这段代码会输出 {"world": 2, "hello": 1, "wonderful": 1} ,因为or_insertmethod返回的是可变引用(&mut V),所以我们可以用*进行dereference,从而改变对应的值本身。

其中split_whitespace就如同它的字面意思,可以根据空白符分割一个字符串并返回一个迭代器。

Hashing Functions

HashMap所用的哈希函数并非最快的哈希函数,但是它提供了安全性。如果恁觉得它原来的哈希函数太慢了,恁可以用BuildHasher将其换成其它哈希函数。此外,crates.io中也有一些轮子。

小结

  • Vector、String和Hash Map可以给存储、获取、修改数据提供很大的方便。

  • standard library中还有不少Vector、String与Hash Map的method,值得一康。

参考

The Rust Programming Language by Steve Klabnik and Carol Nichols, with contributions from the Rust Community : https://doc.rust-lang.org/book/

Rust by Examplehttps://doc.rust-lang.org/rust-by-example/index.html

posted @ 2019-11-13 16:51  wr786  阅读(108)  评论(0编辑  收藏  举报