rust语言基础学习笔记(中)
2020-07-02
1.包、Crate和模块 #
rust的模块系统包括:
- 包: cargo的一个功能,它允许你构建、测试、分享crate。一个包可以包含多个二进制crate和一个可选的库crate
- crate: 一个模块的树形结构,它形成了库或二进制项目
- 模块(mod)和
use
: 模块通过use来使用,允许你控制作用域和路径的私有性 - 路径(path): 一个命名例如结构体、函数或模块等项的方式
1.1 包和crate #
crate是一个二进制项或库。
crate root
是一个源文件,crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块。
crate将一个作用域相关的功能分组到一起,使得这些功能可以方便的在多个项目间共享。
包提供一系列功能的一个或多个crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。 包中所包含的内容由几条规则来确立。一个包中至多 只能包含一个库crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个crate,无论是库的还是二进制的。
cargo new
命令实际上是创建了一个package:
1cargo new c10_package
2 Created binary (application) `c10_package` package
3
4tree c10_package
5c10_package
6├── Cargo.toml
7└── src
8 └── main.rs
此时我们创建了一个只包含src/main.rs的包,表示它只包含一个名称为c10_package
的二进制crate。
如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate(一个库和一个二进制项),且名字都与包相同。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。
1.2 定义模块控制作用域和私有性 #
模块让我们可以将一个crate中的代码进行分组,以提高可读性与重用性。模块还可以控制项的私有性,crate中所有项(模块、函数、结构体、方法、枚举、常量)都是私有的,需要使用pub
修饰才能暴露给外部。
创建一个lib可以通过命令cargo new --lib libname
来进行。我们在c10_package
下使用cargo new --lib restaurant
创建一个新的名称为restaurant
的库:
1c10_package
2├── Cargo.toml
3├── restaurant
4│ ├── Cargo.toml
5│ └── src
6│ └── lib.rs
7└── src
8 └── main.rs
在lib.rs中创建模块:
1pub mod front_of_house {
2 pub mod hosting {
3 pub fn add_to_waitlist() {}
4
5 pub fn seat_at_table() {}
6 }
7
8 pub mod serving {
9 pub fn take_order() {}
10
11 pub fn server_order() {}
12
13 pub fn take_payment() {}
14 }
15}
16pub fn eat_at_restaurant() {
17 // 绝对逻辑使用模块中的项
18 crate::front_of_house::hosting::add_to_waitlist();
19 // 相对路径使用模块中的项
20 front_of_house::hosting::add_to_waitlist();
21}
1.3 使用use关键字将名称引入作用域 #
1use crate::front_of_house::hosting;
2use crate::front_of_house::hosting::add_to_waitlist as add_to;
3
4
5pub fn eat_at_restaurant() {
6 hosting::add_to_waitlist();
7 add_to();
8}
2.常见集合 #
2.1 vector #
vector允许在一个单独的数据结构中储存多于一个的值,所有值在内存中彼此相邻排列。vector中只能储存相同类型的值。
创建vector的方式:
1let mut nums :Vec<i32> = Vec::new();
2
3let nums = vec![1, 2, 3];
向vector中添加元素:
1let mut v = Vec::new();
2v.push(5);
3v.push(6);
4v.push(7);
5v.push(8);
丢弃vector时也会丢弃其所有元素:
1 let str = String::from("hello");
2{
3 let mut v :Vec<String> = Vec::new();
4 v.push(str)
5} // v离开作用域,其中的str也被一起被丢弃
6println!("{}", str); // 编译错误 borrow of moved value: `str`
读取vector中的元素, 方法1索引语法:
1let one :&i32 = &nums[0];
2println!("nums[0]={}", one);
3// nums[3]; // 索引越界会panic
读取vector中的元素, 方法2get
方法:
1match nums.get(1) {
2 Some(value) => println!("{}", value),
3 None => println!("none"),
4}
5match nums.get(3) {
6 Some(value) => println!("{}", value),
7 None => println!("none"),
8}
遍历vector中元素:
1// vector中添加元素
2let mut nums :Vec<i32> = Vec::new();
3nums.push(2);
4nums.push(4);
5nums.push(6);
6
7// 遍历vector(可变方式)
8for num in &mut nums {
9 print!("{}, ", num);
10 *num += 1;
11}
12println!();
13
14// 遍历vector(不可变方式)
15for num in &nums {
16print!("{}, ", num);
17}
18println!();
使用枚举来储存多种类型:
1enum SpreadsheetCell {
2 Int(i32),
3 Float(f64),
4 Text(String),
5}
6
7let row = vec![
8 SpreadsheetCell::Int(3),
9 SpreadsheetCell::Text(String::from("blue")),
10 SpreadsheetCell::Float(10.12),
11];
2.2 字符串(String) #
Rust的核心语言中只有一种字符串类型: str
(字符串slice
),它通常以被借用形式出现&str
。
字符串slice
它们是一些存储在别处的UTF-8编码字符串数据的引用。比如字符串字面值被存储在二进制输出中,字符串slice也是如此。
而String
类型是由标准库提供,它没有被写进Rust核心语言部分,它是可增长的、可变的、有所有权的、UTF-8编码的字符串类型。
Rust中的字符串是作为字节集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。String
是一个Vec<u8>
的封装。
当我们提到Rust中的字符串时可能是 &str
,也可能是String
。
新建字符串:
1let mut s = String::new();
2s.push_str("hello");
3println!("{}", s);
4let s = String::from("world");
5println!("{}, {}", s, s.len()); // 5
6let s = String::from("世界");
7println!("{}, {}", s, s.len()); // 6
更新字符串:
1let mut s = String::from("hello");
2s.push_str(" world");
3s.push('!');
4println!("{}", s);
5
6// 合并两个String
7let s1 = String::from("hello");
8let s2 = String::from("world");
9let s3 = s1 + " " + &s2;
10// s1的所有权移交给s3, 此时不能再使用s1
11// println!("{}", s1) // value borrowed here after move
12println!("{}", s3);
13
14let s1 = String::from("hello");
15let s2 = String::from("world");
16let s3 = format!("{} {}", s1, s2);
17println!("{}", s3);
遍历字符串:
1let s = "你好";
2// 遍历String - 按字符
3for c in s.chars() {
4 print!("{}, ", c);
5}
6println!();
7// 遍历String - 按字节
8for c in s.bytes() {
9 print!("{}, ", c);
10}
11println!();
2.3 hash map #
创建HashMap:
1use std::collections::HashMap;
2
3
4let mut h :HashMap<String, i32>= HashMap::new();
5h.insert("k1".to_string(), 1);
6h.insert("k2".to_string(), 2);
访问HashMap:
1match h.get("k2") {
2 Some(v) => println!("v = {}", v),
3 _ => println!("none"),
4}
5
6if let Some(v) = h.get("k1") {
7 println!("v = {}", v);
8}
遍历:
1for (k, v) in &h {
2 println!("{}:{}", k, v)
3}
常用操作:
1h.entry("k1".to_string()).or_insert(100); // k1键不存在时插入
2
3
4let words_line = "go rust python java groovy scala python java groovy ";
5let mut word_count_map :HashMap<String, i32> = HashMap::new();
6for word in words_line.split_whitespace() {
7 let count = word_count_map.entry(String::from(word)).or_insert(0);
8 *count += 1;
9}
10for (k, v) in &word_count_map {
11 println!("{}: {}", k, v);
12}
3.错误处理 #
rust提供了很多特性来处理出现错误的情况。
rust语言将错误分为两个类别: 可恢复错误(recoverable)和不可恢复错误(unrecoverable):
- 可恢复错误通常代表用户报告错误和重试操作是合理的情况,例如未找到文件。rust中使用枚举类型
Result<T, E>
来实现。 - 不可恢复错误是bug的同义词,例如尝试访问超过数组结尾的位置。rust中通过
panic!
来实现
3.1 panic!
与不可恢复错误
#
rust提供了一个panic!
宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后直接退出。
1#[test]
2#[should_panic(expected = "crash and burn")]
3fn panic_showcase() {
4 panic!("crash and burn")
5}
1#[test]
2#[should_panic(expected = "index out of bounds: the len is 3 but the index is 3")]
3fn panic_showcase_vector() {
4 let mut v = vec![1, 2, 3];
5 v[3] = 100
6}
3.2 Result
与可恢复的错误
#
Result<T, E>
枚举的定义大致如下:
1enum Result<T, E> {
2 Ok(T),
3 Err(E),
4}
Ok(T)
的T
代表成功时返回的结果,Err
的E
代表失败是返回的错误。
1#[test]
2#[should_panic]
3fn error_handling() {
4 use std::fs::File;
5 let _file = match File::open("notexits.txt") {
6 Ok(file) => file,
7 Err(error) => {
8 panic!("fail to open file: {:?}", error);
9 },
10 };
11}
如果只是需要在失败场景下直接panic的话,
Result<T, E>
提供了unwrap
和expect
来简写上面的Match匹配:
1#[test]
2#[should_panic]
3fn error_handling() {
4 use std::fs::File;
5 let _file = File::open("notexits.txt").unwrap();
6}
1#[test]
2#[should_panic]
3fn error_handling4() {
4 use std::fs::File;
5 let _file = File::open("notexits.txt").expect("fail to open file");
6}
3.3 传播错误 #
传播错误是指如果发生错误时,将其返回给调用者让其决定如何处理错误。
1#[test]
2#[should_panic]
3fn error_propagating() { // 错误传播
4 use std::io;
5 use std::io::Read;
6 use std::fs::File;
7 fn read_username_from_file() -> Result<String, io::Error> {
8 let f = File::open("hello.txt");
9 let mut f = match f {
10 Ok(file) => file,
11 Err(e) => return Err(e),
12 };
13 let mut s = String::new();
14 match f.read_to_string(&mut s) {
15 Ok(_) => Ok(s),
16 Err(e) => Err(e),
17 }
18 }
19
20 read_username_from_file().unwrap();
21
22}
使用?
运算符简写错误传播:
1#[test]
2#[should_panic]
3fn error_propagating2() { // 错误传播 ?运算符简写
4 use std::io;
5 use std::io::Read;
6 use std::fs::File;
7 fn read_username_from_file() -> Result<String, io::Error> {
8 let f = File::open("hello.txt")?;
9 let mut s = String::new();
10 f.read_to_string(&mut s)?
11 Ok(s)
12 }
13
14 read_username_from_file().unwrap();
15
16}
4.泛型、trait #
泛型是具体类型或者其它属性的抽象替代,用于减少代码重复。 使用泛型并不会造成程序上性能的损失。Rust通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。 单态化是通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
4.1 在函数定义中使用泛型 #
1 // 定义泛型函数
2fn largest<T: PartialOrd + Copy>(list :&[T]) -> T {
3 let mut largest = list[0];
4 for &item in list.iter() {
5 if item > largest {
6 largest = item;
7 }
8 }
9 largest
10}
11
12let nums = vec![3, 15, 9, 18, 7, 11];
13let largest_num = largest(&nums);
14println!("largest_num={}", largest_num);
15let nums = vec![3.2, 15.0, 9.0, 18.1, 7.0, 11.0];
16let largest_num = largest(&nums);
17println!("largest_num={}", largest_num);
4.2 在结构体和方法定义中使用泛型 #
1#[derive(Debug)]
2struct Point<T> {
3 x: T,
4 y: T,
5}
6
7impl<T> Point<T> {
8 fn get_x(&self) -> &T {
9 &self.x
10 }
11}
12
13let p1 = Point {
14 x: 10,
15 y: 30,
16};
17println!("p1 = {:#?}, p1.x={}", p1, p1.get_x());
4.3 trait #
trait用于定义与其它类型共享的功能,类似于其它语言中的接口
- 可以通过trait以抽象的方式定义共享的行为
- 可以使用trait bounds指定泛型是任何拥有特定行为的类型