章节1
1.1 变量与可变性
rust的变量声明使用let,通常来说声明一个变量的不可变的,如果需要可变就需要加入mut
let mut x = 5;
x = 6;
如果没有mut,对于x的重新赋值就是有问题的。
变量与常量的区别:
常量可以用mut进行可变的声明,但是常量constant是永远不可变的
声明常量需要使用const关键字,并且必须标注类型
常量可以在任何作用域内进行声明,包括全局作用域
常量的命名必须是全大写的形式
const MAX:u32 = 1000_000;
shadow:如果用let声明一个新的变量,那么这个变量就会覆盖掉先前的变量,这个新的变量可以是新的类型,是相对自由的。
1.2 数据类型:标量类型
数据类型分为标量和复合
Rust是静态编译语言,在编译时必须知道所有变量的类型
静态编译语言是指在程序运行之前,源代码被一次性编译成机器码或中间字节码,然后在运行时不需要再次编译即可直接执行的语言。这种编译方式与动态编译(解释执行)相对,动态编译是在程序运行时边解释边执行源代码。
Rust有四种变量类型:
整型
bool类型
浮点数类型
字符串类型
整数类型
1.3 复合类型
主要有元组tuple和数组(数组和tuple的长度一旦声明就是固定的)
tuple:每个位置对应一个类型,类型不必相同
fn main(){
let tup:(i32,f64,u8) = (500,1.6,5);
println!("{},{},{}",tup.0,tup.1,tup.2);
}
可以使用模式匹配来结构一个tuple来获取元素的值
fn main(){
let tup:(i32,f64,u8) = (500,1.6,5);
let (x,y,z) = tup
println!("{},{},{}",x,y,z);
}
数组
多个值放在一个类型里且类型必须相同。

fn main(){
let x = [1,2,3,4,5]
}
数组和tuple这种固定类型的一般存放在栈stack上。这样会有更快的访问速度,但是如果你希望由灵活的长度那么这个时候你可以考虑使用vector
数组有一些特别的声明方式
let x = [3;5];
let x = [3,3,3,3,3];
rust对于数组的越界有一个简单的检查但是如果是复杂情况下的数组越界编译器是不会发现的
let x = [1,2,3,4,5];
let index = [11,12,13,14,15];
let num = x[index[0]];
上述代码是不会报错的。
1.4 函数与注释
函数的签名里必须声明每个参数的类型
fn main(){
another_function(5);
}
fn another_function(x:u32){
println!("got a number {}",x);
}
函数需要返回值的时候一般可以默认函数段最后的值作为返回值,若想要提前返回则需要使用return语句
fn plus_five(x:i32) -> i32{
x + 5;
}
1.5 控制流if-else
if 根据条件选择语句,这个条件必须是bool类型的
fn main(){
let number = 5;
if number > 5{
println!("more than five");
}else{
println!("less than five");
}
}
如果代码中出现太多的if_else语句那么最好使用match对其进行重构
1.6控制流:循环
rust提供了三种循环:loop,while,for
loop的代码会反复的执行
fn main(){
loop{
println!("hello");
}
}
如果需要停止需要借助break
fn main(){
let mut count = 0;
let result = loop{
count += 1;
if count == 10{
break count*2
}
}
}
while循环
fn main(){
let mut number = 3;
while number != 0{
println!("{}!",number);
number -= 1;
}
println!("lift off")
}
for循环
for循环很适合用来遍历,代码简洁紧凑。
fn main(){
let a = [1,2,3,4,5];
for elenment in a.iter(){
println!("the value is {}",element);
}
}
章节2
2.1 所有权
2.1.1 stack and heap
首先了解一下堆和栈的区别
在编程中,数据存放在堆(heap)和栈(stack)中对运行速度有显著影响,这主要是因为它们的管理方式、分配速度和内存布局的不同。以下是一些关键点,说明了堆和栈存储数据对运行速度的影响:
内存分配速度:
栈:栈内存由操作系统在线程创建时自动分配,并且具有后进先出(LIFO)的特性。栈内存的分配和释放非常快速,因为操作系统会预先为每个线程分配一块固定大小的栈空间,并且维护一个栈指针来追踪当前的栈顶位置。
堆:堆内存的分配和释放通常比栈内存慢,因为它涉及到更复杂的内存管理机制,如内存碎片整理和垃圾回收(对于某些语言)。堆内存的分配需要查找足够大的连续内存块,并可能需要更新内存分配表。
内存大小和生命周期:
栈:栈空间的大小通常有限,且每个线程的栈空间是独立的。栈上的数据在函数调用时创建,在函数返回时销毁,因此它们的生命周期与函数调用直接相关。
堆:堆空间通常比栈空间大得多,并且所有线程共享。堆上的数据的生命周期不固定,它们可以在程序的任何地方被创建和释放,且通常由程序员或垃圾回收器管理。
访问速度:
栈:由于栈内存的连续性和操作系统的优化,栈上数据的访问速度通常较快。
堆:堆内存可能包含内存碎片,且由于其动态分配的特性,访问速度可能不如栈内存快。不过,现代计算机的内存管理技术已经非常高效,因此这种差异可能不明显。
内存管理开销:
栈:栈内存的管理开销较小,因为它遵循严格的LIFO规则,且不需要程序员手动管理。栈内存的分配和释放是自动的。
堆:堆内存的管理开销较大,尤其是对于需要垃圾回收的语言。垃圾回收机制需要定期运行,以清理不再使用的对象,这个过程可能会导致程序暂停执行,从而影响运行速度。
性能优化:
栈:由于栈内存的快速分配和释放,以及访问速度的优势,程序员通常会尽量将局部变量和短暂的数据结构存储在栈上。
堆:对于需要长时间存活的对象,或者大小不确定的数据结构(如链表、树等),使用堆内存更为合适。虽然堆内存的分配和释放较慢,但它们提供了更大的灵活性和更长的生命周期。
rust的核心就是所有权
rust的内存是通过一个所有权的系统来管理的,其中包含一组编译器在编译时检查的规则,程序运行的时候所有权的特性不会减慢程序的运行速度。
思考天地
stack上的存储由于我们可以直接找到栈顶地址也就是通过寄存器进行访问所以可以迅速的找到我们需要存储的地址,但是heap的存储需要找到一块足够大的地方来进行存储,所以我们存储heap时会导致有找寻空间的开销。
2.1.2 所有权规则、内存和分配
所有权规则:
每个值都有一个变量,这个变量是该值的所有者,每个值只能同时拥有一个所有者,超出作用域的值就会湮灭。
String类型:
字符串字面值:程序里手写的哪些字符串值,他们是不可变的。
Rust还有第二种字符串类型:String
使用字面值的速度会更快因为在编码的时候就已经知道大小了,所以叫做字面值
fn main(){
let s1 = 'hello';
let s2 = String::from("world");
s2.push_str("this world");
}
字面值是不可修改的,但是String类型则是可以修改的。
一个String的Inode由三个部分组成,指向字符串的指针,容量和长度,这个Inode大小是可以确定的存放在stack上,而存放字符串的内容则在heap上。
注意如下代码:
fn main(){
let s1 = String::from("hello");
let s2 = s1;
}
s1赋给s2本身是一个move操作,也就是移交了所有权,此时的s1已经失效了,也无法再进行调用。
但是这种move类型的赋值并不是绝对的,因为如果是实现了copy trait的类型,进行这样的赋值就会直接进行一份赋值,因为存放在了stack上面所以分配很快。但如果没有实现copy trait的类型希望能够进行拷贝可以调用clone方法。
2.1.3 所有权与函数
请看如下代码:
fn main(){
let s1 = String::from("hello");
let x1 = 5;
copy(x1);
take_ownership(s1);
println!("I still own {}",x1);
}
fn copy(x:u32){
println!("get {}",x);
}
fn take_ownership(x:String){
println!("get {}",x);
}
2.2 引用和借用
&的本质就是取得某些值但不获得其所有权
在特定的作用域内只能有一个可变的引用
fn main(){
let x = 5;
let s1 = &mut x;
let s2 = &mut x;
}
这是就因为多个可变引用导致报错,主要的目的是为了防止数据竞争
但是如果两个可变引用的生命周期没有重叠就不会存在问题
并且不可以同时具有不可变和可变的引用
2.3 切片
字符串字面值是切片
fn main(){
let s = String::from("hello world");
let s1 = &s[0..5];
let s2 = &s[6..];
}
字符串切片代替字符串的引用会更加通用
章节3
3.1 struct例子
想要使用struct必须为结构体定义实例。
struct user{
username:String,
id:u64,
active:bool,
}
fn main(){
let mut user1 = user{
username:String::from("boby"),
active:true,
id:10086,
};
user1.name = String::from("mike");
}
tuple struct
struct color(i32,i32,i32);
struct Point(i32,i32,i32);
struct里面的数据可以自己拥有所有权,也可以放置一个引用,只需要保证引用的生命周期要大于等于这个结构体的生命周期就好,引用必须比结构体活的更长
3.2struct例子
关注一下属性宏
#[derive(Debug)]是Rust中的一个属性宏,用于自动为结构体或枚举类型实现std::fmt::Debug trait,这样它们的实例就可以使用{:?}格式化符号进行打印,以便于调试和查看结构体的状态。
struct Rectangle{
width:i64,
lenth:i64,
}
impl Rectangle{
fn area(&self)->i64{
self.width * self.lenth
}
}
fn main(){
let rect = Rectangle{
width:60,
lenth:50,
}
let x = rect.area();
println!("I got {}",x);
}
关联函数
在impl块里不把self作为第一个参数的函数叫做关联函数
struct Rectangle{
width:i64,
lenth:i64,
}
impl Rectangle{
fn area(&self)->i64{
self.width * self.lenth
}
fn square(size:i64)->Rectangle{
Rectangle{
width:size,
lenth:size,
}
}
}
fn main(){
let rect = Rectangle{
width:60,
lenth:50,
}
let x = rect.area();
println!("I got {}",x);
}
章节4
4.1 定义枚举
enum Ipkind{
V4,
V6,
}
enum Ipkind_plus{
v4(u8,u8,u8.u8),
v6(String),
}
4.2 Option枚举
标准库中的定义如下
enum Option<T>{
Some(T),
None,
}
这个模块包含在预导入模块中,可以直接使用。
4.3 match
绑定值的模式匹配
enum state{
califonia,
alabama,
}
enum coin{
penny,
nickel,
dime,
quarter(state) ,
}
fn value_of_coin(coin:coin)->u8{
match coin{
coin::penny => {
println!("penny!");
1
}
coin::nickel => 5,
coin::dime => 10,
coin::quarter(state) =>{
println!("from {}",state),
25
}
}
}
fn main(){
let x = coin::quarter(alabama);
println("{}",value_of_coin(x));
}
Option模式匹配
fn main(){
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
fn plus_one(x:Option<i32>)->Option<i32>{
match x {
None => None,
Some(i) => Some(i+1),
}
}
match匹配必须穷举所有的可能
4.4if_let语法糖
fn main(){
let v = Some(0u8);
match v{
Some(3) => println!("three!!"),
_ => println!("others"),
}
if let Some(3) = v{
println("three");
}
}
章节5
5.1 package、crate、Module
不怎么明白到底怎么回事
一般来说 cargo new project,这个project就是所谓的package
crate的作用在于将相关的功能组合到一个作用域内,便于在项目间进行共享
5.2 路径
如果将函数或者某个结构体存放到一个mod里,那么他就是私有的。
5.3 路径2
使用super可以进入上级目录
注意下上面的树形图可以理解是为什么
枚举如果是公共的,那么里面的字段默认就是公共的。
5.4 use关键字
5.5 将模块内容放到其他文件
章节6
6.1 Vector
存储在堆上的数据是可以动态变化的,无需再编译时就确定
初始值创建vector就可以使用vec!
fn main(){
let v = vec![1,2,3];
}
6.2 Vector+enum的例子
6.2 String类型
rust的核心语言只有字符串切片
fn main(){
let x = "hello rust!";
let s = x.to_string();
println!("{}",x);
let s1 = "another string".to_string();
}
字符串切片可以转化为String类型,上述代码中需要注意,to_string()方法并不会消耗原有的内容
下面的代码实现了字符串的数据添加
let x = String::from("hello");
x.push_str("rust")
format!
可以用于字符串拼接
6.3 hashmap
一般的创建hashmap的方法
用collect创建hashmap的方法
hashmap进行插入的时候,如果插入的类型实现了copy trait那么就可以直接进行复制,如果没有,比如说string类型则会移交所有权,插入也可以插入引用,如果使用引用则需要保证hashmap有效期间其引用的数据需要存活,否则会导致内存不安全,也就是所谓的悬空指针
章节7
7.1 不可恢复错误与panic!
Rust并没有提供所谓的异常机制,因此对于错误我们分为两类去进行处理。
不可恢复的错误panic和可恢复的错误Result<T,E>
panic的操作有两种:
1.通过回溯栈清理数据来解决,有一定的时间代价。
2.通过终止程序直接清理掉函数调用栈。
7.2 可恢复的错误和Result<T,E>
unwrap_or_else
unwrap
expect
和unwrap十分类似,但是使用expect可以更方便的定位到错误发生的位置
请注意这段代码
use std::io:: Read;
use std::io;
use std::fs::File;
fn read_username_from_file() ->Result<String,io::Error>{
let f = File::open("hello.txt");
let mut f = match f{
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
//传入一个string类型s的可变引用,然后把读取到的内容写入s
match f.read_to_string(&mut s){
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
?运算符号传播错误
?运算符号只能应用于返回类型是Option或者Result类型的函数,如果你需要在主函数里使用?
那么需要进行如下修改
use std::error::Error;
use std::fs::File;
fn main() -> Result<(),Box<dyn Error>>{
let f = File::open("hello.txt")?;
Ok(())
}
Box 是一个trait对象,表示任何可能的错误类型
章节8
8.1 泛型
占位符来替代所需要的参数
struct的类型参数可以和方法的类型参数不同
rust使用泛型类型的效率和正常参数是一样的,因为这些工作会在编译时完成。
8.2 trait共享行为
trait:就是抽象的定义共享行为
trait bound:泛型类型参数指定为实现了特定行为的类型
番外篇n
n.1 dashmap
并发性:dashmap 利用了锁分段技术(lock-stripping),将哈希表分割成多个段,并在每个段上使用独立的锁。这样可以减少锁的竞争,提高并发性能,特别是在多核处理器上。
高性能:dashmap 旨在提供高性能的读写操作。通过减少锁的竞争和优化内存布局,dashmap 能够在多线程程序中提供快速的哈希表访问。
内存安全:作为 Rust 语言的一部分,dashmap 保证了内存安全。它使用 Rust 的所有权和生命周期机制来确保数据在并发访问时不会出现内存泄漏或数据竞争。
易用性:dashmap 的 API 设计得类似于 Rust 标准库中的 HashMap,使得开发者可以轻松地从 HashMap 迁移到 dashmap,而无需大幅修改现有代码。
灵活性:dashmap 提供了多种配置选项,允许开发者根据具体需求调整其行为。例如,可以调整锁的类型、哈希表的初始容量等。
use dashmap::DashMap;
fn main() {
let mut map = DashMap::new();
// 插入数据
map.insert("key", "value");
// 读取数据
if let Some(value) = map.get(&"key") {
println!("The value is: {}", value);
}
// 更新数据
*map.get_mut(&"key").unwrap() = "new value";
// 删除数据
map.remove(&"key");
}
为何dashmap实现了并发安全?
锁分段(Lock Striping):
DashMap 使用锁分段技术来提高并发性能。在这种技术中,哈希表被分割成多个段(shards),每个段由一个独立的锁保护。这样,多个线程可以同时访问不同的段,从而减少了锁竞争。当一个线程尝试修改一个段时,它只需要获取该段的锁,而不会影响其他段的访问。
读写锁(Read-Write Locks):
对于读操作,DashMap 允许多个线程同时进行,而不会影响其他读操作或写操作的并发安全性。这是通过使用读写锁实现的,其中读锁可以被多个线程共享,而写锁是互斥的。当一个线程获得写锁时,它会阻塞其他读和写操作,直到写操作完成。
内部数据结构:
DashMap 的内部实现使用了一些高效的数据结构,如哈希表和链表,来存储键值对。这些数据结构被设计为在多线程环境下高效地进行读写操作。例如,每个段中的键值对通过链表连接,这样即使在高并发的情况下,也能快速地添加或删除元素。
无锁数据结构(Lock-Free Data Structures):
DashMap 在某些操作上使用了无锁数据结构,这意味着它避免了使用传统的锁机制。无锁数据结构通过原子操作来保证数据的一致性和线程安全,从而在高并发场景下提供更好的性能。
线程安全的数据访问:
DashMap 提供了一系列线程安全的方法,如 insert、remove、get 等,这些方法在内部都会正确地处理并发访问。开发者不需要手动管理锁,只需调用这些方法即可安全地在多线程环境中操作 DashMap。
优化的迭代器:
DashMap 提供了迭代器,允许线程安全地遍历整个哈希表或其一部分。迭代器在内部使用了读锁,确保在迭代过程中不会修改哈希表,从而保证了并发安全。
// 读取操作示例
let value = dashmap.get(&1).expect("Key not found");
println!("Read value: {}", value);
// 写入操作示例(需要获取锁)
dashmap.insert(1, "New value".to_string());
在Rust中,dashmap.get(&1)是调用Dashmap库提供的get方法来获取与指定键(在这个例子中是整数1)关联的值的引用。这个方法返回的是一个选项类型(Option),其中包含了与键关联的值的引用,如果键不存在,则返回None。
这里的&符号表示取引用,因为get方法需要一个键的引用作为参数。在Rust中,引用是一种指针,它允许你在不拥有值的情况下访问值。这通常用于避免在传递数据时复制整个数据结构,从而提高性能。
dashmap更新
key:1 value
use dashmap::DashMap;
let mut map: DashMap<i32, String> = DashMap::new();
map.insert(1, "one".to_string()); // 插入或替换键为 1 的值
智能指针
Box
:最简单的智能指针,用于在堆上分配内存,拥有大型数据结构或者转移数据所有权但是不确大小的时候
fn main(){
let b = Box::new(5);
println!("b={}",b)
}
需要注意到过度使用导致内存碎片化,影响程序性能。
Rc
:引用计数指针,允许多个所有者,用于读取同一数据但是不进行修改的场景。
use std::rc::Rc;
fn main(){
let a = Rc::new(5);
let b = a.clone;
let c = a.clone;
println!("a = {},b={},c={}",a,b,c)
}
Arc
:Arc是Rc的线程安全版本,用于多线程场景下的数据共享
注意到Arc大概率也是不修改
use std::sync::Arc;
use std::Thread;
fn main(){
let a = Arc::new(5);
let b = a.clone();
let handle = thread::spawn(move ||{
println!("Thread b = {}",b);
});
println!("Main a = {}",a);
handle.join().unwrap;
}
RefCell
:提供了内部可变性,允许在有不可变引用时改变数据。
use std::cell::RefCell;
fn main(){
let a = RefCell::new(5);
*a.borrow_mut() +=1;
println!("a={}",a.borrow());
}
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(5);
// 借用并读取值
let borrowed_value = cell.borrow();
println!("借用的值是: {}", borrowed_value);
// 借用并修改值
if let Ok(mut value) = cell.try_borrow_mut() {
*value += 1;
println!("修改后的值是: {}", value);
}
}
format!
format! 宏可以在编译阶段就计算出字符串的值,并将其实例化为一个常量。这样做的好处是,如果格式化字符串中的表达式是已知的并且在运行时不会改变,那么使用 format! 可以提高程序的性能,因为编译器可以内联这个字符串常量,避免运行时的字符串创建和复制操作。
let name = "world";
let greeting = format!("Hello, {}!", name);
hashmap
hashmap的创建
//hashmap创建
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
}
也可以用collect进行创建
use std::collections::HashMap;
fn main() {
let name = vec!["ftz","zky"];
let age = vec![30, 18];
let person: HashMap<_,_> = name.iter().zip(age.iter()).collect();
println!("{:?}",person);
}
注意:!!!
迭代器的使用不会消耗(consume)原始的集合(如向量)。这意味着,当你创建一个迭代器并使用它进行遍历时,原始的集合仍然保持不变,且其中的所有元素都还在。这是因为迭代器只是提供了一种访问集合元素的方式,而不涉及对集合本身的修改。
let numbers = vec![1, 2, 3, 4, 5];
// 创建一个迭代器
let mut iter = numbers.iter();
// 使用迭代器,并消耗它来收集元素到一个新的向量中
let collected: Vec<i32> = iter.collect();
// 此时,迭代器 `iter` 已经被消耗,无法再次使用
// 下面的代码将会编译错误,因为 `iter` 已经到达了末尾
// println!("Next value: {}", iter.next());
迭代器在进行一次遍历之后就需要重新创建一个迭代器。
为什么不使用for循环进行遍历呢?
对于简单的遍历和访问集合元素,直接遍历可能更合适。但是,如果你需要进行复杂的数据处理,使用迭代器可能会更加方便和高效。此外,现代编译器和 Rust 的优化机制通常能够减少迭代器带来的开销,因此在很多情况下,性能差异不会太显著。
迭代器的优势如下:
抽象和通用性:迭代器提供了一种抽象,使得你可以以相同的方式遍历不同类型的集合,而不需要关心集合的内部实现。这种通用性使得编写更灵活、更可复用的代码成为可能。
链式调用:迭代器允许你将多个操作串联起来,形成方法调用链。这样可以构建复杂的逻辑,同时保持代码的清晰和简洁。
惰性求值:许多迭代器操作是惰性求值的,这意味着它们只在需要时才计算下一个值,从而可以节省资源。例如,filter、map、fold 等操作都可以延迟计算,直到消费者请求值。
并发和并行:某些迭代器操作专门为并发和并行处理设计,如 par_iter 和 par_iter_mut,它们可以在多核处理器上并行执行迭代操作,提高性能。
内存效率:迭代器允许你在不复制整个集合的情况下访问元素,这在处理大型数据集时尤其有用。例如,迭代器可以按需访问数据,而不是一次性将所有数据加载到内存中。
不消耗原始数据:迭代器不会消耗(consume)它所遍历的集合,这意味着你可以在迭代完成后继续使用原始集合。
选择更新或者插入
哈希 map 有一个特有的 API,叫做 entry,它获取我们想要检查的键作为参数。entry 函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。Entry 的 or_insert 方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
person.entry(String::from("ftz")).or_insert(31);
person.entry(String::from("ftzSon")).or_insert(1);
println!("{:?}",person);
}
遍历hashmap:
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
for (key,value) in &person {
println!("the {} age is {}",key,value);
}
}
hashmap所有权
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
let ftz_name = String::from("ftz");
let ftz_age = 30;
person.insert(ftz_name, ftz_age);
println!("the {} age is {}",ftz_name,ftz_age);
}
对于像 i32 这样的实现了 Copy trait 的类型,其值可以拷贝进哈希 map。对于像 String 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
let ftz_name = String::from("ftz");
let ftz_age = 30;
person.insert(&ftz_name, ftz_age);
println!("the {} age is {}",ftz_name,ftz_age);
}
功能性代码
use chrono::{
DateTime, Utc};
pub fn convert_timestamp_to_datetime_str(tp: i64) -> Option<String> {
// 尝试从 Unix 时间戳创建一个 DateTime<Utc> 对象
if let Some(t) = DateTime::<Utc>::from_utc(chrono::NaiveDateTime::from_timestamp(tp, 0)) {
// 使用指定的格式创建一个格式化的日期时间字符串
let result = t.format("%y-%m-%d %H:%M:%S").to_string();
// 返回格式化的日期时间字符串,包装在 Some 中
Some(result)
} else {
// 如果转换失败,返回 None
None
}
}