本文属于我的 Rust 学习笔记 系列。
Rust 入门学习笔记以实际例子为主,讲解部分不是从零开始的,所以不建议纯萌新观看,读者最好拥有任意一种面向对象语言的基础,然后自己多多少少看过 Rust 的基本语法,刷过一点 rustlings。
来源:原子之音。当然也包含个人的一些补充。
视频
代码
Rust 进阶学习笔记以及实战的来源则五花八门,将会标注在下一行⬇️。
本节出处:原子之音 视频 代码
概述
复合数据类型(Compound types)通过组合基础数据类型,来表达更复杂的数据结构。也就是使用其他类型定义的类型。
也被称为派生类型。
内置复合类型
- 结构体 struct
- 数组 array
- 元组 tuple
- 动态数组 vector
标准库中的复合类型
来自std::collections
:
- VecDeque
- LinkedList
- HashMap, BTreeMap
- HashSet, BTreeSet
字符串与字符串切片
字符串
Rust 中 String 的源码如下:
#[derive(PartialEq, PartialOrd, Eq, Ord)]
#[stable(feature = "rust1", since = "1.0.0")]
#[lang = "String"]
pub struct String {
vec: Vec<u8>,
}
可以看到,Rust 中的字符串被设计成一个内容为 u8 可变数组的结构体,其中 u8 是一个无符号整型,可以表示 0~255 之间的数字。
二进制到字符串
从二进制数组到字符串的过程,需要解决两个问题:
- 字符串什么时候结束
- 存储结束符号(C 语言,非常不安全)
- 在开头存储字符串长度(Rust)
- 采用什么编码
- ASCII 一个字节对应一个字符,可以表示常用的 256 个字符
- UTF-8 一到四个四届对应一个字符,可以辨识所有语言的字符,且可以和 ASCII 兼容。
因为一个字节就是一个 8 位二进制数,所以 String 才使用了 u8。
Rust 字符串
- 在开头存储字符串长度
- 所有都是 UTF-8
- 默认不可变
string_item
存储了(详见下文 Vec 源码):
- pointer:字符串头一个字符的地址
- length:具体字符个数
- capacity:容量,扩容一般会翻倍
字符串切片
str 就是字符串切片,实际上是 [u8] 数组切片,也就是动态大小的 UTF-8 字节。
str 在 Rust 中无法直接使用(str 是不安全的),经常用的是&str
,以及Box<str>
、Rc<str>
等智能指针。
区别
String 具有所有权,&str 是切片引用。
String 的数据必须存储在堆上,&str 则根据引用指向,可能存储在堆上、栈上甚至数据段上。
在创建需要所有权的字符串、有修改需求时,使用 String;在查找、只读,以及不需要关心生命周期时,使用 &str。
例子
fn rust_say() -> &'static str {
r#"Rust said "str" is danger"#
}
fn u8_to_string(string_data: &[u8]) -> String {
string_data.iter().map(|&s|s as char).collect()
}
fn main() {
let mut item = String::new();
println!("String {}: cap {}", item, item.capacity()); item.push('c');
println!("String {}: cap {}", item, item.capacity()); let item = String::with_capacity(10);
println!("String {}: cap {}", item, item.capacity());
let item = String::from("hello world");
println!("String {}: cap {}", item, item.capacity());
let item = "hello world".to_string();
println!("String {}: cap {}", item, item.capacity());
let i = &item;
println!("{}", rust_say());
const C_S: &str = "";
let yzzy = "yzzy";
println!("u8 to String: {}", u8_to_string(yzzy.as_bytes()));
for item in yzzy.as_bytes() {
println!("item {}", item); }
}
元组和数组
元组可以包含各种类型值的组合,数组只能包含统一类型数据的组合。
默认不可变。
fn u8_to_string(string_data: &[u8]) -> String {
string_data.iter().map(|&s|s as char).collect()
}
#[derive(Debug)]
struct User{
name: String,
age: i32,
}
fn main() {
let tup = (1,2,"hello", User{name: "y".to_owned(), age: 45});
println!("tup {} {} {} {:?}",tup.0, tup.1, tup.2, tup.3 );
let (a, b, c, d) = tup;
println!("a {} b {} c {} d {:#?}", a, b, c, d);
let my_array = [1,2,3,4];
let my_slice = &my_array[1..3];
println!("my_array len {}", my_array.len());
println!("my_slice len {}", my_slice.len());
let u8_array = [121u8, 122u8, 122u8, 121u8];
let u8_slice_ref = &u8_array[0..4]; println!("{:#?}", u8_to_string(&u8_array));
println!("{:#?}", u8_to_string(u8_slice_ref));
}
序列类型
Vec
可变数组(vector,又是译作“矢量”)存储在堆上,在运行时可以增加或减少数组长度。
数组默认是不可变的。
Vec 堆栈存储
Vec 的源码如下。其中,buf
在堆上,其他在栈上。
pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
buf: RawVec<T, A>,
len: usize,
}
pub(crate) struct RawVec<T, A: Allocator = Global> {
inner: RawVecInner<A>,
_marker: PhantomData<T>,
}
struct RawVecInner<A: Allocator = Global> {
ptr: Unique<u8>,
cap: Cap,
alloc: A,
}
用途
- 在不知道应该选择哪种集合结构时,首选就是 Vec。
- 需要一个长度实时变化的数据存储单元时
- 希望存储单元有序时
- 希望内存中各个元素的存储地址连续时
例子
#[derive(Debug, Clone)]
struct Car {
id: i32,
name: String
}
fn main() {
let my_vec = vec![2, 3, 4];
println!("{:#?}: {}", my_vec, my_vec.capacity()); let my_vec = vec![2;3];
println!("{:#?}: {}", my_vec, my_vec.capacity()); let my_vec:Vec<i32> = Vec::new();
println!("{:#?}: {}", my_vec, my_vec.capacity()); let my_vec:Vec<i32> = Vec::with_capacity(5);
println!("{:#?}: {}", my_vec, my_vec.capacity());
let mut cars = Vec::new();
for i in 1..11 {
let car_type = if i % 2 == 0 {"car"} else {"bus"};
cars.push(Car {
id: i,
name: format!("{}-{}", car_type, i)
});
}
println!("{:?}", cars);
let car = cars.pop().unwrap();
println!("{:?}", cars);
println!("{:?}", car); let car = cars.remove(0);
println!("{:?}", cars);
println!("{:?}", car); cars.push(car.clone());
println!("{:?}", cars);
cars.insert(0, car.clone());
println!("{:?}", cars);
cars[9] = Car{
id: 11,
name: "car-11".to_string()
};
println!("{:?}", cars);
let item = cars.get(0);
println!("{:?}", item);
let item = cars.get_mut(9).unwrap();
*item = Car{id:10, name: "car-10".to_owned()};
println!("{:?}", cars);
let mut cars2 = vec![car.clone(), car.clone()];
cars.append(&mut cars2);
println!("{:?}", cars);
println!("{:?}", cars2);
let car_slice = cars.as_slice();
println!("{:?}", car_slice);
cars.clear();
println!("{:?}", cars);
println!("{:?}", cars.is_empty());
}
VecDeque
VecDeque
是一个双向动态数组,两端都可以以O(1)的时间复杂度插入和移除。
VecDeque
在内存中并不一定是连续的。
用途
有双向增删需求的,如队列。
方法
VecDeque 继承了 Vec 的所有方法。此外还新增了如下方法:
- push_back/push_front
- pop_back/pop_front
- contains
- front/front_mut
- back/back_mut
例子
use std::collections::VecDeque;
#[derive(Debug, Clone)]
struct Car {
id: i32,
name: String
}
fn main() {
let mut int_queue: VecDeque<i32> = VecDeque::new();
println!("{:?} cap {}", int_queue, int_queue.capacity()); int_queue.push_back(3);
println!("{:?} cap {}", int_queue, int_queue.capacity()); let mut int_queue: VecDeque<i32> = VecDeque::with_capacity(20);
println!("{:?} cap {}", int_queue, int_queue.capacity());
let mut cars = VecDeque::from([
Car{id: 1, name: String::from("Car-1")},
Car{id: 2, name: String::from("Car-2")},
Car{id: 3, name: String::from("Car-3")},
]);
println!("{:?} cap {}", cars, cars.capacity());
cars.push_back(Car { id: 4, name: "Car-4".to_string() });
println!("{:?} cap {}", cars, cars.capacity());
cars.push_front(Car { id: 0, name: "Car-0".to_string() });
println!("{:?} cap {}", cars, cars.capacity());
let back_car = cars.pop_back();
println!("{:?}", back_car);
println!("{:?} cap {}", cars, cars.capacity());
let front_car = cars.pop_front();
println!("{:?} ", front_car);
println!("{:?} cap {}", cars, cars.capacity());
println!("{:?}", cars.front());
println!("{:?}", cars.back());
println!("car{:?}", cars[1]);
let car = cars.get(1).unwrap();
println!("car{:?}", car);
let car = cars.get_mut(1).unwrap();
*car = Car{id: 10, name: "Car-n".to_owned()};
println!("car{:?}", cars);
cars.clear();
println!("{:?}", cars.is_empty());
}
LinkedList
LinkedList 是一种通过链式存储数据的序列数据结构,是双向的,并且具有所有权。
用途
LinkedList 在分割和追加上较为高效。
如果对分配多少内存和何时分配内存有细粒度的控制要求时,可以考虑使用。
绝大多数情况下更应该直接使用 Vec 和 VecDeque。
方法
LinkedList 支持大多数 VecDeque 的方法,不支持 Vec 的方法。
例子
use std::collections::LinkedList;
#[derive(Debug, Clone)]
struct Car {
id: i32,
name: String
}
fn main() {
let mut int_link: LinkedList<i32> = LinkedList::new();
let mut cars = LinkedList::from([
Car{id: 1, name: "Car-1".to_string()},
Car{id: 2, name: "Car-2".to_string()},
Car{id: 3, name: "Car-3".to_string()},
]);
cars.push_back(Car{id: 4, name: "Car-4".to_string()});
println!("back: {:?}", cars.back());
cars.push_front(Car{id: 0, name: "Car-0".to_string()});
println!("front: {:?}", cars.front());
println!("{:?}", cars);
let car = cars.pop_back().unwrap();
println!("{:?}", car); let car = cars.pop_front().unwrap();
println!("{:?}", car);
let mut car_list = cars.split_off(cars.len() -2);
println!("{:?}", car_list); println!("{:?}", cars); cars.append(&mut car_list);
println!("{:?}", cars);
println!("{:?}", car_list);
cars.clear();
println!("{}", cars.is_empty());
}
键值对 Map
键值对又称为字典数据类型,主要是为了查找创建的数据结构。
HashMap
HashMap<K, V> 类型存储了一个键类型 K 和一个值类型 V 的映射。它通过一个哈希函数(Hash Function,也叫散列函数)来决定如何存储键和值,实现映射。
HashMap 和 Vec 一样,数据也在堆上。
HashMap 无序的。
用途
- 仅次于 Vec 的第二选择
- 存储映射关系
- 要求较快查找速度(O(1))
- 需要内存缓存
键
HashMap 对键有特殊要求,即必须满足哈希函数。
所以需要实现Hash
、Eq
、PartialEq
等特质。
对于普通的结构体,可以通过增加#[derive(Debug, Hash, Eq, PartialEq)]
自动实现。
例子
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Car {
id: i32,
price: i32
}
fn main() {
let _int_map:HashMap<i32, i32> = HashMap::new();
let _int_map:HashMap<i32, i32> = HashMap::with_capacity(10);
let mut car_map = HashMap::from([
("Car1", Car{id: 1, price: 10000}),
("Car2", Car{id: 2, price: 4000}),
("Car3", Car{id: 3, price: 890000}),
]);
for (k, v) in &car_map {
println!("{k}:{:?}", v);
}
println!("Some {:?}", car_map.get("Car1"));
println!("None {:?}", car_map.get("Car6"));
car_map.insert("Car4", Car { id: 4, price: 80000 });
println!("{:?}", car_map);
car_map.insert("Car4", Car { id: 5, price: 300000 });
println!("{:?}", car_map);
car_map.entry("Car4").or_insert(Car{id: 9, price: 9000});
println!("{:?}", car_map); car_map.remove("Car4");
println!("{:?}", car_map);
car_map.entry("Car4").or_insert(Car{id: 9, price: 9000});
println!("{:?}", car_map);
let mut car_map = HashMap::from([
(Car{id: 1, price: 10000}, "Car1"),
(Car{id: 2, price: 4000}, "Car2"),
(Car{id: 3, price: 890000}, "Car3"),
]);
println!("Car2: {:?}\n", car_map.get(&Car{id: 1, price:10000}));
for (car, name) in &car_map {
println!("{:?}: {name}", car)
}
car_map.retain(|c, _| c.price < 5000);
println!("< 5000 {:?}", car_map);
}
BTreeMap
BTreeMap 是一种有序形式的键值对,内部基于 BTree 构建。
用途
- 需要有序 map
- 查找时可以通过二分提高性能
- 有序的代价:缓存效率和查找性能的折中
键
因为需要排序,所以 BTreeMap 需要实现Ord
、PartialOrd
。
例子
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Car {
id: i32,
price: i32
}
impl Ord for Car {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.price.cmp(&other.price)
}
}
impl PartialOrd for Car {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.price.cmp(&other.price))
}
}
fn main() {
let _int_map:BTreeMap<i32, i32> = BTreeMap::new();
let mut car_map = BTreeMap::from([
("Car1", Car{id: 1, price: 10000}),
("Car2", Car{id: 2, price: 4000}),
("Car3", Car{id: 3, price: 890000}),
]);
println!("{:#?}", car_map);
let mut car_map = BTreeMap::from([
(Car{id: 1, price: 10000}, 1),
(Car{id: 2, price: 4000}, 2),
(Car{id: 3, price: 890000}, 3),
]);
for (k, v) in &car_map {
println!("{:?}: {v}", k); }
car_map.insert(Car{id: 4, price: 90000}, 4);
for (k, v) in &car_map {
println!("{:?}: {v}", k);
}
println!("{:?}", car_map.get(&Car{id: 1, price: 10000}));
println!("{:?}", car_map.first_key_value());
println!("{:?}", car_map.last_key_value());
let car = car_map.pop_first().unwrap();
println!("{:?}", car);
let car = car_map.pop_last().unwrap();
println!("{:?}", car);
for (k, v) in &car_map {
println!("{:?}: {v}", k);
}
car_map.remove(&Car{id: 1, price: 10000});
for (k, v) in &car_map {
println!("{:?}: {v}", k);
}
car_map.clear();
println!("{}", car_map.is_empty());
}