Rust | 02. 包管理、集合(String、Vector、HashMap)、泛型
240108 Package, Crate, Module
- 代码组织需要包括哪些细节可以暴露,哪些细节可以私有,作用域内哪些名称有效等等;Rust的代码组织(也可以叫模块系统)如下:
- Package(包),是Cargo的特性,让你构建、测试、共享crate
- Crate(单元包),一个模块树,它可以产生一个library或可执行文件
- Module(模块)、use,让你控制代码的组织、作用域、私有路径
- Path(路径),是为Struct、function、module等项命名的方式
Package和Crate
-
Crate有两种类型,即binary或library;Crate root是指源代码文件,Rust编译器会从这里开始,组成你Crate的根Module;一个Package包含了一个Cargo.toml文件(用于描述了如何构建这些Crates),一个package只能包含0-1个library crate,以及任意数量的binary crates,另外,一个library至少包含一个crate(library或binary)
-
通常而言,src/main.rs是binary crate的crate root,且crate名与package名相同;而在library crate中,则会生成一个叫做src/lib.rs的文件,其是该library crate的crate root,此时crate的名字也与package名相同;一个package中也可以同时这两个文件,此时该package中包含两个crate,一个binary一个library,都与package名相同;一个package可以有多个binary crate,文件放在src/bin目录下,每个文件是一个单独的binary crate
-
Crate的作用:将相关功能组合到一个作用域内,便于在项目间共享,防止冲突;例如rand crate,就可以使用它的名字访问其相关功能
-
定义Module可以控制作用域和私有性:在一个crate内,将代码进行分组,增强可读性,易于复用,可以使用public、private关键字控制项目(item)的私有性;使用mod关键字建立module,其可以嵌套,类似于如下的实例
1 2 3 4 5 6 7 8 9 10 11 12
mod front_of_house{ mod hosting{ fn add_to_waiting(){ } fn seat_at_table(){ } } mod serving{ fn take_order(){ } fn serve_order(){ } fn take_payment(){ } } }
路径 path
-
路径用于在rust中找到某个模块,有两种形式:
- 绝对路径,从crate root开始,使用crate名或字面值"crate"
- 相对路径,从当前模块开始,使用self、super或当前模块标识符
-
路径至少由一个标识符组成,标识符之间用::分隔,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/lib.rs mod front_of_house{ // 需要声明公开,否则无法外部访问 pub mod hosting{ pub fn add_to_waiting(){ } } } pub fn eat_at_restaurant(){ // 使用绝对路径,从顶部crate起始,逐级向下(使用crate字面量) crate::front_of_house::hosting::add_to_waiting(); // 相对路径,因为和顶级模块在同一级,可从模块名开始调用 front_of_house::hosting::add_to_waiting(); }
-
PS:定义的位置和使用的位置始终一起移动时,可以选择使用相对路径;但为了避免错误,有经验的程序员会直接写成绝对路径
私有边界和pub关键字
-
模块不仅仅可以组织代码,还可以定义私有边界;Rust中的所有条目(函数、方法、struct、模块、常量)都是私有的;父级模块无法访问子模块中的私有条目,而在子模块中可以使用所有祖先模块中的条目
-
使用pub关键字可以使模块对外暴露;根级模块可以相互调用,无论共有还是私有
-
super关键字用于访问父级模块中的内容,相当于文件系统中的两个点..
1 2 3 4 5 6 7 8
fn serve_order(){ } mod back_of_house{ fn fix_incorrect_order(){ cook_order(); super::serve_order(); //相对路径 } fn cook_order(){ } }
-
当将pub关键字放在struct前,struct就是公共的,但字段如果没有加pub修饰时,字段仍旧是私有的;即是说,struct的字段需要单独设置pub来变成共有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast{ toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } } pub fn eat_at_restaurant(){ let mut meal = back_of_house::Breakfast::summer("Rye"); // 使用关联函数初始化 meal.toast = String::from("Wheat"); //pub公共字段可以修改 println!("I'd like {} toast please", meal.toast); meal.seasonal_fruit = String::from("Blueberries"); // 这一句会报错,因为season_fruit是私有的 }
-
使用pub关键字在enum前,可以把枚举类型变为公共类型,需要注意的是,当枚举类型为公共时,其变体默认也为公共类型(只有公共的才有用)
use关键字
-
可以使用use关键字将路径导入到当前作用域内(类似Linux的符号链接),但仍旧遵循私有原则
1 2 3 4 5 6 7 8 9 10 11 12 13 14
mod front_of_house { pub mod hosting { pub fn add_to_waitlist(){ } } } use crate::front_of_house::hosting; // 使用绝对路径 use front_of_house::hosting; // use也可以使用相对路径 pub fn eat_at_restaurant(){ hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); }
-
在上面的案例中,有经验的程序员在引入一个函数时,是引入到函数的上一级,而不是直接到函数名(
crate::front_of_house::hosting::add_to_waitlist
),这是想要强调,该函数是在hosting模块下的,如果直接引入函数名,则会与本地定义函数混淆、无法区分 -
但对于引入struct、enum类型时,可以直接指定完整路径到其本身;但对于同名条目,还是需要指定到父级,否则会混淆
1 2 3 4 5
use std::collections::HashMap; fn main(){ let mut map = HushMap::new(); map.insert(1, 2); }
1 2 3 4 5
use std::fmt; use std::io; fn f1() -> fmt::Result { } fn f2() -> io::Result{ } fn main() { }
-
as关键字可以为引入的路径指定一个本地的别名
1 2 3 4 5
use std::fmt::Result; use std::io::Result as IoResult; fn f1() -> Result { } fn f2() -> IoResult { } fn main() { }
-
使用use将路径(名称)导入到作用域后,该名称在此作用域内是私有的。例如下面的代码,函数
eat_at_restaurant()
对于外部代码而言是公开的;但名称hosting却对外面是默认私有的,需要在use语句前使用pub关键字,将其公开1 2 3 4 5 6 7 8 9 10 11 12 13
mod front_of_house { pub mod hosting { pub fn add_to_waitlist(){ } } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant(){ hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); }
-
小结一下,pub use的作用就是重导出,其不仅仅将条目引入当前作用域,也使该条目可以被外部代码引入到他们的作用域内
-
可以使用嵌套语句,来在同一行内同时引入相同包/模块下的多个条目,其格式为:
use 路径相同部分::{路径, 不同, 部分} 1 2 3 4
use std::cmp::Ordering; use std::io; use std::{cmp::Ordering, io};
1 2 3 4
use std::io; use std::io::Write; use std::io::{self, Write};
-
可以使用通配符*,可以将路径中所有公共条目都引入到当前作用域内;需要注意的是,通配符需要谨慎使用,其仅仅在测试阶段(将所有需要测试代码引入到tests模块)或预导入模块(prelude)
1
use std::collections::*;
外部包(package)
-
在
Cargo.toml
文件中添加外部包的名字和版本号,cargo build时就会自动从https://crates.io/下载到本地环境,随后便可使用use将特定条目引入到作用域 -
可以在
\Users\user\.cargo\config
目录下写配置文件来更换清华源1 2 3 4 5 6 7 8 9
[source.crates-io] registry "https://github.com/rust-lang/crates.io-index" replace-with 'tuna' [source.tuna] registry "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" [net] git-fetch-with-cli true
-
Rust的标准库std也被当做是外部包,需要使用use引入特定条目到当前作用域,但因其本身写入到rust语言本体之中,不需要修改
Cargo.toml
文件来包含std
模块拆分到不同文件
-
定义模块时,模块名后可以直接写分号
;
,而不是代码块,此时Rust会从与模块同名的问价你中加载内容(即实现了将模块拆分到不同的文件内)1 2 3 4 5 6 7 8
src ├─ lib.rs │ mod front_of_house{ │ pub mod hosting;{ │ pub fn add_to_waitlist(){ } │ } │ } └─ main.rs
1 2 3 4 5 6 7 8
src ├─ front_of_house.rs │ pub mod hosting;{ │ pub fn add_to_waitlist(){ } │ } ├─ lib.rs │ mod front_of_house; └─ main.rs
1 2 3 4 5 6 7 8 9
src ├─ lib.rs │ mod front_of_house; ├─ main.rs ├─ front_of_house.rs │ mod hosting; └─ front_of_house └─ hosting.rs pub fn add_to_waitlist(){ }
240109 常用集合
Vector
-
可以使用Vector类型存储类型相同的多个值,写作Vec<T>,其由标准库提供
1 2 3 4
fn main(){ let v: Vec<i32> = Vec::new(); // 因为初始化时为空,编译器无法推断类型,需要手动标注i32类型 }
1 2 3 4
fn main(){ let v = vec![1, 2, 3]; // 使用宏vec!直接初始化,此时不需要显式指定类型 }
-
使用
push()
函数向其中添加元素1 2 3 4 5
fn main(){ let v = Vec::new(); v.push(1); // 由于向其中添加了值,此时编译器可以推断类型 v.push(2); }
-
使用索引或
get()
方法获得其中的元素,索引从0开始,超出索引范围后会引发程序崩溃,而get方法返回Option枚举类型,需要手动处理1 2 3 4 5 6 7 8 9 10
fn main(){ 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 elemrnt"), } }
-
所有权和借用规则要求,不能在同一作用域内同时拥有可变和不可变引用,该规则仍旧适合vector;因为Vector的其元素成集合统一存储在heap上,因而不能只考虑一个元素是否可变,而需要关注整个vector是否可变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
fn main(){ let mut v = vec![1,2,3,4,5]; let first = &v[0]; v.push(6); // 因为前一行定义为不可变引用,这一行会报错 println!("The first element is {}", first); // 需要修改时,需要界定为可变引用 for i in &mut v{ *i += 50; } for i in v{ println!("{}", i); } }
-
在Vector中只能存放相同类型的数据,当有需求存储不同类型数据时,可以基于枚举的如下特性,利用附加数据的枚举类型来完成
-
枚举的变体可以附加不同类型的数据
-
枚举变体的定义在同一个枚举类型下
-
PS:在使用该方法定义Vector时,需要知道详尽的数据类型,不能存在类型未知的情况;Trait对象对于解决该问题有帮助
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(3.5), ] }
-
字符串 String
-
字符串是Byte的集合,能够提供一些方法将byte解析为文本的类型,在Rust中,通常意义上所说的字符串是指String类型或&str类型;此外,标准库中也还提供了许多其他的字符串类型,如OsString、OsStr、CString、CStr等
-
据说Rust的字符串很劝退,原因可能在于字符串本身的复杂性、Rust倾向于暴露可能的错误、Rust使用utf-8编码
-
在Rust的核心语法层面,只有一个字符串类型,是字符串切片(str或&str),其是对储存在其他地方、utf-8编码的字符串的引用
-
而字符串String是来自标准库,是可增长、可修改、可拥有的类型;由于其本身就是Byte的集合,因而许多Vec<T>的操作也都可用于String,可以使用String::new()创建字符串,也可以直接使用
to_string()
方法(该方法可用于实现了Display trait的类型,包含字符串字面值)或String::from()
函数创建初始化一个字符串1 2 3 4 5 6
fn main(){ let data = "initial contents ⭐"; let s1 = data.to_string(); let s2 = String::from("initial contents"); }
-
String的大小是可以增减的,其内容也可以更改
-
使用
push_str()
方法可以向字符串尾部添加内容,该方法的参数是字符串切片(因而可以直接填写字面值),因为是传入引用,是借用字符串,不获取其所有权,因而修改后的字符串仍旧可以正常使用 -
使用
push()
方法,可以把单个字符添加到字符串尾部1 2 3 4 5 6
fn main(){ let mut s = String::from("foo"); s.push_str("bar"); s.push('x'); println!("{}", s); }
-
使用
+
运算符直接拼接字符串,其相当于使用了类似签名fn add(self, s:&str) -> String {...}
这样的方法(实际上是一个泛型,会更复杂),因而需要加号前为字符串,加号后为字符串切片,在相加后前面的变量所有权移交到add内部,离开后会失效,而后面的变量则可以继续保留所有权而可以继续使用1 2 3 4 5 6 7
fn main(){ let s1 = String::from("Hello, "); let s2 = String::from("World!"); let s3 = s1 + &s2; println!("{}", s3); }
PS:在该例子中,加号后面使用
&s2
是字符串引用类型,并不是上面要求的字符串切片,这是因为Rust存在一个解引用强制转换(deref coercion)的机制 -
也可以使用
format!
宏来完成字符串的拼接,其更灵活也不会取得任何参数的所有权1 2 3 4 5 6 7 8
fn main(){ let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); println!("{}", s); }
-
-
按照索引语法访问String的某部分时会报错,即是说Rust字符串不支持索引语法访问;实际上,String是对Vec<u8>的包装,可以使用
len()
方法返回其字节长度1 2 3 4 5 6 7
fn main(){ let len1 = String::from("abcdefg").len(); let len2 = String::from("æɑðɜɣʝʪ").len(); println!("{}, {}", len1, len2); // 输出为7, 14 }
之所以输出24,是因为里面的每个字母占用了2字节;这里所谓的字母称之为Unicode标量值,即是说,rust中不是所有的字节索引都能对应到一个有效的Unicode标量值,因而Rust在开发早期就为了杜绝此类错误,直接拒绝了使用索引值访问
-
Rust有三种看待字符串的方式:字节Bytes、标量值Scalar values和字形簇Grapheme cluster;其中字形簇才是最接近我们所理解的“字母”;可以使用
bytes()
、chars()
方法获得其字节和标量值的迭代器,用于遍历字符串;对于字形簇的获取,其很复杂,标准库为提供1 2 3 4 5 6 7 8 9 10 11
fn main(){ let s = "æɑðɜɣʝʪ"; for b in s.bytes(){ println!("{}", b); } for b in s.chars(){ println!("{}", b); } }
-
此外,Rust不允许对String进行索引,还因为每次索引操作都消耗一个常量时间O(l),而String无法保证这个时间,因为要确认这个时间需要遍历整个字符串来计算有多少个合法字符
-
可以使用[]和一个范围来创建字符串的切片(切割字符串),但是需要谨慎使用,如果切割时跨越了字符边界,程序就会panic
1 2 3 4 5
fn main(){ let s = "æɑðɜɣʝʪ"; let s1 = &s[0..4]; //可以切割出两个字符 let s2 = &s[0..3]; //编译不会报错,运行会报错,说索引3不是一个字符的边界 }
-
小结,Rust选择将正确处理String作为程序员的默认行为,要求程序员必须在处理utf-8数据之前投入更多精力,从而防止在开发后期处理涉及非ASCII字符时的错误
HushMap
- 哈希表(HashMap)是以键值对的形式存储数据的一种方式,相当于python或C#中的字典
如果你觉得这篇文章对你有帮助,欢迎赞赏~
赞赏