47

Rust入坑指南:鳞次栉比

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU0NTkzODUyMw%3D%3D&%3Bmid=2247484118&%3Bidx=1&%3Bsn=5985a08d3961d62e4d9cc8062b9a38e8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

很久没有挖Rust的坑啦,今天来挖一些排列整齐的坑。没错,就是要介绍一些集合类型的数据类型。“鳞次栉比”这个标题是不是显得很有文化?

在Rust入坑指南:常规套路一文中我们已经介绍了一些基本数据类型了,它们都存储在栈中,今天我们重点介绍3种数据类型:string,vector和hash map。

String

String类型我们在之前的学习中已经有了较多的接触,但是没有进行过详细的介绍。有些有编程基础的同学可能不屑于学习String类型,毕竟它在所有编程语言中可以说是最常用的类型了,大家也都很熟悉了。对于有这种心理的同学,我想对他们说:我刚开始也是这样想的,直到后来我被编译器揍的满头包,才下定决心回来认真学习一下String类型。

Rust的字符串分为以下几种类型:

  • str:表示固定长度的字符串

  • String:表示可增长的字符串

  • CStr:表示由C分配,被Rust借用的字符串,一般用于和C语言交互

  • CString:表示由Rust分配并且可以传递给C语言的字符串

  • OsStr:表示和操作系统相关的字符串,主要为了兼容Windows

  • OsString:OsStr的可变版本

  • Path:表示路径

  • PathBuf:是Path的可变版本

本文我们重点讨论前两种,因为它们是开发过程中最常用的,也是比较容易混淆的。对于str,我们常见的是它的引用类型, &str 。如果你看过了Rust入坑指南:核心概念一文后,相信你已经了解了引用类型和Ownership的概念。也就是说String类型具有Ownership而&str没有。

在Rust中,String本质上是Vec<u8>,Vec是向量集合的关键字,我们在后面会介绍。String类型由三个部分组成,分别是:指向堆中字节序列的指针,记录堆中字节序列的长度和堆分配的容量。通过一段代码也许你很有更深的理解。

1fn main() {
2    let mut a = String::from("foo");
3    println!("{:p}", a.as_ptr());
4    println!("{:p}", &a);
5    assert_eq!(a.len(), 3);
6    a.reserve(10);
7    assert_eq!(a.capacity(), 13);
8}

在这段代码中我们可以看到,a.as_ptr()获取指针和&a获取的指针是不一样的。

vaEbuuQ.png!web

rust06-1

这里我们解释一下,as_ptr获取到的指针是堆中字节序列的指针地址,而&a的地址是字符串变量在栈上的指针地址。另外,len()和capacity()方法得到的长度都是字节数量,而非字符数量。这里你可以自己动手试试中文字符的长度。

聊完了字符串的基本概念以后,相信你已经对Rust的字符串有了一个大概的认识。接下来我们就一起来看一看字符串的CRUD的方法吧。

创建字符串

话不多说,先来展示一下创建字符串的各种方法。

1fn main() {
2    let string: String = String::new();
3    let string: String = String::from("hello rust");
4    let string: String = String::with_capacity(10);
5    let str: &'static str = "Jackey";
6    let string: String = str.to_owned();
7    let string: String = str.to_string();
8}

我们比较常用的是前两种,下面介绍一下后面几个方法。with_capacity()是创建一个空字符串,参数表示在堆中分配的字节数。to_owned和to_string是演示了如何把&str类型转换成String类型。

修改字符串

Rust修改字符串的常用方法也有很多,例如在字符串后追加,连接两个字符串,更新字符串等。下面这段代码就展示了一些修改字符串的方法。

 1fn main() {
 2    let mut hello = String::from("Hello, ");
 3    hello.push('J');    // 追加单个字符
 4    hello.push_str("ackey! ");    //追加字符串
 5    println!("push: {}", hello);
 6
 7    hello.extend(['M', 'y', ' '].iter());   //追加多个字符,参数为迭代器
 8    hello.extend("name".chars());
 9    println!("extend: {}", hello);
10
11    hello.insert(0, 'h');   //类似于push,可以指定插入的位置
12    hello.insert(1, 'a');
13    hello.insert(2, '!');
14    hello.insert_str(0, "Haha");
15    println!("insert: {}", hello);
16
17    let left = "Hello, ".to_string();
18    let right = "World".to_string();
19    let result = left + &right;
20    println!("+: {}", result);   //使用+连接字符串时,第二个必须为引用
21    let mut message = "rust".to_string();   //使用+=连接字符串时,字符串必须定义为可变
22    message += "!";
23    println!("+=: {}", message);
24
25    let s = String::from("foobar");
26    let s: String = s
27        .chars()
28        .enumerate()
29        .map(|(_i, c)| {c.to_uppercase().to_string()})
30        .collect();
31    println!("update chars: {}", s);
32
33    let s1 = String::from("hello");
34    let s2 = String::from("rust");
35    let s3 = format!("{}-{}", s1, s2);
36    println!("format: {}", s3);
37}

我们对上面的代码做一些补充的解释。

push和insert类似,带有_str的方法接收的参数是字符串,否则只能接收单个字符。insert可以指定插入的位置,而push只能在字符串末尾插入。

使用「+」连接字符串时,第一个参数是String类型,第二个则需要是引用类型&str。这类似于我们调用一个add方法,它的定义是这样的:

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

所以,第一个参数的ownership转移到了函数中,又通过返回结果传递出来。也就是说,在使用了+操作符之后, left 已经没有ownership了。

字符串查找

在Rust中,字符串是不能根据位置来获取到指定字符的。也就是下面这段代码是编译不过的。

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

因为,Rust会认为这个0是指第一个字节,而Rust字符串中的字符可能占有多个字节(还记得前面我让你用中文字符实验代码吗?)所以,如果你单纯的想要获取一个字节,编译器不知道你是真的想要获取字节对应的数值,还是要获取那个字符。

我们在处理字符串时通常有以下方法:

 1fn main() {
 2    let hello = "Здравствуйте";
 3    let s = &hello[0..4];
 4    println!("{}", s);
 5
 6    let chars = hello.chars();
 7    for c in chars {
 8        println!("{}", c);
 9    }
10
11    let bytes = hello.bytes();
12    for byte in bytes {
13        println!("{}", byte);
14    }
15
16    let get = hello.get(0..1);
17    let mut s = String::from("hello");
18    let get_mut = s.get_mut(3..5);
19
20    let message = String::from("hello-world");
21    let (left, right) = message.split_at(6);
22    println!("left: {}, right: {}", left, right);
23}

通常是使用字符切片,也可以使用chars方法获取到Chars迭代器,然后可以对每个字符进行单独处理。此外,使用get或get_mut方法也可以接收索引范围,返回指定的字符串切片。返回结果是Option类型,这是因为如果指定的索引返回不能返回完整字符,那么Rust就会返回None。这里也可以使用is_char_boundary方法来判断一个位置是否是非法边界。

最后,也可以使用split_at或split_at_mut方法来分割字符串。这要求分割的位置正好是字符边界位置,如果不是,程序就会崩溃。

删除字符串

Rust的标准库提供了一些删除字符串的方法,我们来演示一些:

 1fn main() {
 2    let mut hello = String::from("hello");
 3    hello.remove(3);
 4    println!("remove: {}", hello);
 5    hello.pop();
 6    println!("pop: {}", hello);
 7    hello.truncate(1);
 8    println!("truncate: {}", hello);
 9    hello.clear();
10    println!("clear: {}", hello);
11}

结果如图:

QB3MV3Y.png!web

rust06-2

remove方法用来删除字符串中的某个字符,其接收的参数是字符的起始位置,如果是不是某个字符的起始位置,会导致程序崩溃。

pop方法会弹出字符串末尾的字符,truncate方法是截取指定长度字符串,而clear方法则是用来清空字符串。

至此,关于Rust中的字符串的基本概念和CRUD我们都已经介绍完了,接下来我们再来看另一种集合类型Vector。

Vector

Vector是用来存储相同数据类型的多个数据一种数据类型。它的关键字是 Vec<T> 。下面我们一起来看看向量的CRUD吧。

创建向量

1fn main() {
2    let v1: Vec<i32> = Vec::new();
3    let v2 = vec![1, 2, 3];
4}

上面这段代码演示了创建一个向量的两种方式,第一种是使用new函数来创建一个空的向量,由于没有添加元素,所以要显式的指定存储元素的类型。第二种是创建一个有初始值的向量集合,我们直接使用vec!宏,然后指定初始值即可,不需要指定向量中元素的数据类型,因为编译器可以自己推断出来。

更新向量

1fn main() {
2    let mut v = Vec::new();
3    v.push(1);
4    v.push(2);
5}

创建一个空的向量之后,如果我们想要增加元素,就可以直接使用push方法,向末尾追加元素。

删除向量

 1fn main() {
 2    let mut v = Vec::new();
 3    v.push(1);
 4    v.push(2);
 5    v.push(3);
 6
 7    v.pop();
 8    for i in &v {
 9        println!("{}", i);
10    }
11}

删除单个元素可以使用pop方法,而要删除整个向量,只能像其他结构体一样,到其ownership失效。

读取向量元素

 1fn main() {
 2    let v = vec![1, 2, 3, 4, 5];
 3
 4    let third: &i32 = &v[2];
 5    println!("The third element is {}", third);
 6
 7    match v.get(2) {
 8        Some(third) => println!("The third element is {}", third),
 9        None => println!("There is no third element."),
10    }
11
12    let v = vec![100, 32, 57];
13    for i in &v {
14        println!("{}", i);
15    }
16}

当你需要读取单个指定元素时,有两种方法可以用,一种是使用 [] ,另一种是使用get方法。两种方法的区别是:第一种返回的是元素的类型,而get返回的是Option类型。如果你指定的位置越界了,那么使用第一种方法程序会直接崩溃,而使用第二种方法则会返回None。

此外,还可以通过遍历向量的形式来读取元素。如果想要存储不同类型的数据,我们可以借助枚举类型。

 1fn main() {
 2    enum SpreadsheetCell {
 3        Int(i32),
 4        Float(f64),
 5        Text(String),
 6    }
 7
 8    let row = vec![
 9        SpreadsheetCell::Int(3),
10        SpreadsheetCell::Text(String::from("blue")),
11        SpreadsheetCell::Float(10.12),
12    ];
13}

HashMap

HashMap存储了KV结构的数据,各个Key必须是同一种类型,各个Value必须是同一种类型。由于HashMap是三种集合类型中使用最少的,所以在使用之前,需要手动引入进来

1use std::collections::HashMap;

创建HashMap

首先我们来了解一下如何创建一个新的Hash Map并增加元素。

1use std::collections::HashMap;
2fn main() {
3    let field_name = String::from("Favorite color");
4    let field_value = String::from("Blue");
5
6    let mut map = HashMap::new();
7    map.insert(field_name, field_value);
8}

注意,在使用insert方法时, field_namefield_value 都会失去所有权。那如何再使用它们呢?我们只能从Hash Map中再拿出来。

访问Hash Map的数据

 1use std::collections::HashMap;
 2fn main() {
 3    let field_name = String::from("Favorite color");
 4    let field_value = String::from("Blue");
 5
 6    let mut map = HashMap::new();
 7    map.insert(field_name, field_value);
 8
 9    let favorite = String::from("Favorite color");
10    let color = map.get(&favorite);
11    match color {
12        Some(x) => println!("{}", x),
13        None => println!("None"),
14    }
15}

可以看到,我们使用get可以获取到指定Key的值,get方法返回的是Option类型,如果没有指定的Value,则会返回None。此外,也可以使用for循环来遍历Hash Map。

 1use std::collections::HashMap;
 2fn main() {
 3    let mut scores = HashMap::new();
 4
 5    scores.insert(String::from("Blue"), 10);
 6    scores.insert(String::from("Yellow"), 50);
 7
 8    for (key, value) in &scores {
 9        println!("{}: {}", key, value);
10    }
11}

更新Hash Map

当我们向同一个Key insert值时,旧的值就会被覆盖。如果只想要在Key不存在时插入,则可以使用entry。

 1use std::collections::HashMap;
 2fn main() {
 3    let mut scores = HashMap::new();
 4    scores.insert(String::from("Blue"), 10);
 5
 6    scores.entry(String::from("Yellow")).or_insert(50);
 7    scores.entry(String::from("Blue")).or_insert(50);
 8
 9    println!("{:?}", scores);
10}

总结

今天带大家一起挖了三个坑,string,vector和hash map,分别介绍了每种数据类型的CRUD。对string的介绍占了比较大的篇幅,因为它是最常用的数据类型之一。当然这部分的相关知识还有很多,欢迎大家和我一起学习交流。

RRnANfI.gif

扫码关注

有趣的灵魂在等你

eArQzym.jpg!web

FvUb2mV.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK