《100 Exercises To Learn Rust》练习笔记

重新再补一下基础

100 Exercises To Learn Rust

一、基础

整数

  • 无自动类型转换,不同类型变量之间赋值会报错
  • 2个整数相除得到的还是整数,比如5/2得到的是2

变量

  • 函数的参数必须显示声明类型

if语句

  • if 表达式中的条件必须是 bool 类型,即布尔值
  • if/else是一个表达式,在rust中表达式可以返回一个值,但要求各个分支返回的类型必须一致
rust 复制代码
let number = 3;
let message = if number < 5 {
    "smaller than 5"
} else {
    "greater than or equal to 5"
};

溢出和下溢

比如两个u8类型数据相加值为256,当发生溢出时,rust会有两种处理方式

  • 拒绝执行
  • wrap around 选择环绕,两个u8相加的值,溢出的值重新从0开始到255
  • overflow-checks可以在配置文件中设置这两种方法的选择
  • 如果需要根据上下文执行不同的运算效果,可以使用下面的方法
    • wrapping_methods
    • saturating_methods
rust 复制代码
let x = 255u8;
let y = 1u8;
let sum = x.wrapping_add(y);
assert_eq!(sum, 0);

let x = 255u8;
let y = 1u8;
let sum = x.saturating_add(y);
assert_eq!(sum, 255);

as类型转换

详细文档

需要注意的这个截断的概念

模块可见性

  • pub:使实体公开 ,即可以从定义它的模块外部访问,可能从其他 crate 访问。
  • pub(crate): 使实体在同一个 crate 中公开,但不在外部。
  • pub(super): 在父模块中公开实体。
  • pub(in path::to::module): 在指定模块中公开实体。

类型布局

这里包括数据对齐,初次了解这个概念,通过deepseek查找了一下答案。

在计算机中数据对齐的作用是什么?

提升内存访问速度(性能)

现代计算机的CPU通过数据总线从内存中读取数据。这个总线是有宽度的(例如32位、64位)。内存系统也通常被设计为在对齐的地址上访问时效率最高。

  • 未对齐访问的代价:如果一个4字节的整数存储在地址0x0003(即没有在4字节边界上对齐),CPU需要执行两次内存访问:

    • 第一次从地址0x0000读取4个字节(获取0x0000-0x0003,包含该整数的第一个字节)。
    • 第二次从地址0x0004读取4个字节(获取0x0004-0x0007,包含该整数的后三个字节)。
    • 然后CPU需要将这两次读取结果中的相关部分拼接起来,才能得到这个整数的值。
  • 对齐访问的优势:如果同一个4字节整数存储在地址0x0004(在4字节边界上对齐),CPU只需一次内存访问即可读取整个整数。

显然,一次操作比两次操作外加拼接要快得多。对于高性能计算,这种差异是至关重要的。

rust 复制代码
//在rust中String类型是由指针,长度,空间三个部分组成的
//每一个字段都是表示usize,所以在64位系统中,usize占用8个字节
//那么String占用内容的大小就是 3*8 = 24
fn string_size() {
        assert_eq!(size_of::<String>(), 24);
}

更详细内容查看,类型布局

引用

这里容易误解的一个点是,并不是所有的指针都指向堆

Rust 中的大多数引用,在内存中表示为指向内存位置的指针。因此,它们的大小与指针的大小相同,即 usize

rust 复制代码
assert_eq!(std::mem::size_of::<&String>(), 8);
assert_eq!(std::mem::size_of::<&mut String>(), 8);

什么是胖指针? 就是带有附加元数据的指针 比如Stirng类型它存储的结构是,有指针,长度,容量。比如&str,它的内存模式是一个指针和一个字符长度

二、Trait

标准库中常用trait

先附上一个关于trait的文档链接tour-of-rusts-standard-library-traits

在标准库中定义的一些关键trait

  • Operator traits (e.g. Add, Sub, PartialEq, etc.)
Operator Trait
+ Add
- Sub
* Mul
/ Div
% Rem
== and != PartialEq
<, >, <=, and >= PartialOrd

算术运算符在std::ops模块,比较运算符在std::cmp模块

  • From and Into, for infallible conversions
  • Clone and Copy, for copying values
  • Deref and deref coercion
    • 通过为类型 T 实现 Deref<Target = U>,您可以告诉编译器 &T&U 在某种程度上是可以互换的。
  • Sized, to mark types with a known size
  • Sized, to mark types with a known size

trait的作用

  • 解锁内置行为,比如上面的运算符trait
  • 扩展trait,向现有的类型添加行为
  • 通用编程,比如泛型
rust 复制代码
pub struct Ticket {
    title: String,
    description: String,
    status: String,
}
// 存在一个疑问这里为什么不是&self.title.trim()
impl Ticket {
    pub fn title(&self) -> &str {
        self.title.trim()
    }

    pub fn description(&self) -> &str {
        self.description.trim()
    }
}

Sized

动态大小类型

要点

  • str也是动态大小类型
  • Rust 有一个特定的 trait 来确定一个类型的大小是否在编译时可知,即Sized
  • 对于动态大小类型,必须放在指针后面来使用
  • 每次具有泛型类型参数时,编译器都会隐式假定它是 Sized
  • 当函数绑定动态大小类型时,可以使用?Sized,表示可能是或可能不是Sized
rust 复制代码
// 注意t的类型是&T因为其类型可能不是 `Sized` 的,所以需要将其置于某种指针之后,所以使用了引用
fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

Into和From

  • From和Into是双重Trait,实现了From的任何类型,都实现了Into
  • into()转换成什么类型,需要显示类型标注
rust 复制代码
pub struct WrappingU32 {
    value: u32,
}
impl From<u32> for WrappingU32{
    fn from(value:u32)->Self{
        WrappingU32{value}
    }
}

fn example() {
    let wrapping: WrappingU32 = 42.into();
    let wrapping = WrappingU32::from(42);
}

泛型和关联类型

关联类型使得只有一个实现,在这里定义trait时,type Output是在实现trait时需要确定的类型,Exponent=Self默认值是Self,在使用时可以更改为需要实现的类型,使得与self关联一起

rust 复制代码
pub trait Power<Exponent = Self> {
    type Output;

    fn power(&self, n: Exponent) -> Self::Output;
}

impl Power<u16> for u32 {
    type Output = u32;

    fn power(&self, n: u16) -> Self::Output {
        self.pow(n.into())
    }
}

impl Power<&u32> for u32 {
    type Output = u32;

    fn power(&self, n: &u32) -> Self::Output {
        self.power(*n)
    }
}

impl Power<u32> for u32 {
    type Output = u32;

    fn power(&self, n: u32) -> Self::Output {
        self.pow(n)
    }
}

Clone

它的方法 clone 采用对 self 的引用,并返回一个相同类型的新拥有的实例。

rust 复制代码
pub trait Clone {
    fn clone(&self) -> Self;
}

Copy

rust 复制代码
pub trait Copy: Clone { }

如果类型实现了 Copy,则无需调用 .clone() 来创建该类型的新实例:Rust 会隐式地为您完成此作。

  • 无论 T 是什么,&mut T 从不实现 Copy

类型必须满足一些要求才能被允许实现 Copy

  • 除了它在内存中占用的std::mem::size_of字节之外,该类型不管理任何额外的资源(例如堆内存、文件句柄等)。
  • 该类型不是可变引用 (&mut T)。

在实现Copy时,可以派生它,但必须也派生Clone,因为Copy是Clone的子trait,所以这个类型也必须的视线Clone功能,下面示例说明:

rust 复制代码
#[derive(Copy, Clone)]
struct MyStruct {
    field: u32,
}

Drop

rust 复制代码
pub trait Drop {
    fn drop(&mut self);
}

Drop与Copy

如果一个类型管理超出其在内存中占用的 std::mem::size_of 字节之外的其他资源,则它无法实现 Copy

编译器如何知道类型是否管理其他资源? 没错: Droptrait实现!

如果您的类型具有显式的 Drop 实现,则编译器将假定您的类型附加了其他资源,并且不允许您实现 Copy

下面的实现将报错

rust 复制代码
impl Drop for MyType {
    fn drop(&mut self) {
        todo();
    }
}

Error

rust 复制代码
pub trait Error: Debug + Display {}

TryFrom和TryInto

TryFromTryInto 都在 std::convert 模块中定义,就像 FromInto 一样。

如果为某个类型实现 TryFrom,则可以自动获得 TryInto

rust 复制代码
pub trait TryFrom<T>: Sized {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub trait TryInto<T>: Sized {
    type Error;
    fn try_into(self) -> Result<T, Self::Error>;
}

在这里还是有点困惑的,没有写出来,真的动手了,发现这个章节好几个写不出来,这里的思路挺好的,先实现&str转换到Status的,然后再实现String转换到Status的

rust 复制代码
#[derive(Debug, PartialEq, Clone)]
enum Status {
    ToDo,
    InProgress,
    Done,
}
#[derive(Debug,thiserror::Error)]
#[error("{invalid_status} is not a valid status")]
struct ParseStatusError{
    invalid_status:String
}
impl TryFrom<String> for Status{
    type Error=ParseStatusError;

    fn try_from(value: String) -> Result<Self, Self::Error>{
       value.as_str().try_into()
    }
}
impl TryFrom<&str> for Status {
    type Error = ParseStatusError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value.to_lowercase().as_str() {
            "todo" => {
                Ok(Status::ToDo)
            },
            "inprogress" => Ok(Status::InProgress),
            "done" => Ok(Status::Done),
            _ => Err(ParseStatusError {
                invalid_status: value.to_string(),
            }),
        }
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use std::convert::TryFrom;

    #[test]
    fn test_try_from_string() {
        let status = Status::try_from("ToDO".to_string()).unwrap();
        assert_eq!(status, Status::ToDo);

        let status = Status::try_from("inproGress".to_string()).unwrap();
        assert_eq!(status, Status::InProgress);

        let status = Status::try_from("Done".to_string()).unwrap();
        assert_eq!(status, Status::Done);
    }

    #[test]
    fn test_try_from_str() {
        let status = Status::try_from("todo").unwrap();
        assert_eq!(status, Status::ToDo);

        let status = Status::try_from("inprogress").unwrap();
        assert_eq!(status, Status::InProgress);

        let status = Status::try_from("done").unwrap();
        assert_eq!(status, Status::Done);
    }
}

三、复杂类型

向量

对于vec!向量存储,可以通过Vec::with_capacity设置容量大小,如果超出内容,它将要求分配器提供新的(更大的)堆内存块,复制元素,并释放旧内存。此作可能成本高昂,因为它涉及新的内存分配和复制所有现有元素。因此对于你能预料的内存大小可以使用Vec::with_capacity进行设置,避免自动扩容,导致性能问题。

rust 复制代码
let mut numbers = Vec::with_capacity(3);
numbers.push(1);
numbers.push(2);
numbers.push(3); // Max capacity reached
numbers.push(4); // What happens here?

Iterator trait

iterator trait的实现

rust 复制代码
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

for循环的本质

rust 复制代码
let mut iter = IntoIterator::into_iter(v);
loop {
    match iter.next() {
        Some(n) => {
            println!("{}", n);
        }
        None => break,
    }
}

IntoIterator trait

并非所有类型都实现 Iterator,但许多类型都可以转换为实现 Iterator 的类型, 这就是 IntoIterator 特征的用武之地:

rust 复制代码
trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}

组合器

  • map 将函数应用于迭代器的每个元素。
  • filter 仅保留满足条件的元素。
  • filter_mapfiltermap组合在一个步骤中。
  • cloned 将引用的迭代器转换为值的迭代器,克隆每个元素。
  • enumerate 返回一个新的迭代器,该迭代器产生 (index, value) 对。
  • skip 跳过迭代器的前 n 个元素。
  • taken 个元素之后停止迭代器。
  • chain将两个迭代器合二为一。

collect

collect 使用迭代器并将其元素收集到您选择的集合中。

collect是泛型的,需要提供类型提示来帮助推断出正确的类型,比如下面,在squares_of_evens标注类型,或者使用turbofish语法来指定类型

rust 复制代码
let numbers = vec![1, 2, 3, 4, 5];
let squares_of_evens: Vec<u32> = numbers.iter()
    .filter(|&n| n % 2 == 0)
    .map(|&n| n * n)
    .collect();
    
let squares_of_evens = numbers.iter()
    .filter(|&n| n % 2 == 0)
    .map(|&n| n * n)
    // Turbofish syntax: `<method_name>::<type>()`
    // It's called turbofish because `::<>` looks like a fish
    .collect::<Vec<u32>>();

Slice

一个示例

rust 复制代码
let numbers = vec![1, 2, 3];
//需要注意:`iter` 不是 `Vec` 的方法!
//它是 `&[T]` 的方法,但你可以对 `Vec` 调用它
//多亏了解引用强制转换。
let sum: i32 = numbers.iter().sum();

Vec<T>&[T] 的关系

  • Vec<T> 是一个可增长的堆分配数组
  • &[T] 是一个切片引用(胖指针,包含数据指针和长度)
  • Vec<T> 可以自动转换为 &[T]

方法查找过程

当你调用 numbers.iter() 时,Rust 编译器会:

rust 复制代码
let numbers = vec![1, 2, 3];
let sum: i32 = numbers.iter().sum();
  1. numbersVec<i32> 类型
  2. Vec<i32> 没有 iter() 方法
  3. Vec<i32> 实现了 Deref<Target = [i32]>
  4. 编译器自动将 &Vec<i32> 转换为 &[i32]
  5. &[i32]iter() 方法

要点:当您需要将对 Vec 的不可变引用传递给函数时,首选 &[T] 而不是 &Vec<T>

rust 复制代码
let array = [1, 2, 3];
let slice: &[i32] = &array;

数组切片和 Vec 切片是同一类型:它们是指向连续元素序列的胖指针。在数组的情况下,指针指向堆栈而不是堆,但在使用切片时这并不重要

可变切片

rust 复制代码
let mut numbers = vec![1, 2, 3];
let slice: &mut [i32] = &mut numbers;
slice[0] = 42; //可以通过切片修改元素

可变切片,Rust 不允许您在切片中添加或删除元素。您将只能修改/替换已经存在的元素。

rust 复制代码
let mut numbers = Vec::with_capacity(2);
let mut slice: &mut [i32] = &mut numbers;
slice.push(1);

Index trait

真的没想到,index也是trait 详细的查看文档

IndexMut

只有当类型已经实现了 Index 时,才能实现 IndexMut,因为它解锁了额外的功能

rust 复制代码
// Slightly simplified
pub trait IndexMut<Idx>: Index<Idx>
{
    // Required method
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

BTreeMap类型

BTreeMap 保证条目按其键排序。

rust 复制代码
// `K` and `V` stand for the key and value types, respectively,
// just like in `HashMap`.
impl<K, V> BTreeMap<K, V> {
    pub fn insert(&mut self, key: K, value: V) -> Option<V>
    where
        K: Ord,
    {
        // implementation
    }
}

线程

在此示例中,第一个生成的线程将反过来生成一个子线程,该子线程每秒打印一条消息。然后第一个线程将完成并退出。当这种情况发生时, 只要整个进程正在运行,它的子线程就会继续运行 。在 Rust 的行话中,我们说子线程的寿命超过了它的父线程。

rust 复制代码
use std::thread;

fn f() {
    thread::spawn(|| {
        thread::spawn(|| {
            loop {
                thread::sleep(std::time::Duration::from_secs(1));
                println!("Hello from the detached thread!");
            }
        });
    });
}

leaking data

这里没有太理解 Box::leak()

std::thread::scope

在 Rust 中,std::thread::scope 是一个用于创建作用域线程的函数,它在 Rust 1.63 版本中稳定。它的主要作用是允许生成能够在当前作用域内借用的线程,确保所有线程在作用域结束前完成执行。

作用

安全的数据借用

作用域线程可以安全地借用栈上的数据,而无需 'static 生命周期要求:

rust 复制代码
use std::thread;

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    thread::scope(|s| {
        // 可以安全地借用 `numbers`,因为作用域确保线程在 numbers 被丢弃前结束
        s.spawn(|| {
            println!("Length: {}", numbers.len());
        });
        
        s.spawn(|| {
            println!("Sum: {}", numbers.iter().sum::<i32>());
        });
    }); // 所有线程在这里保证已经结束
    
    // numbers 仍然可用
    println!("Numbers: {:?}", numbers);
}

自动线程连接

作用域内的所有线程会在作用域结束时自动 join,无需手动管理:

rust 复制代码
use std::thread;

fn main() {
    let mut results = Vec::new();
    
    thread::scope(|s| {
        for i in 0..5 {
            results.push(s.spawn(move || {
                i * 2
            }));
        }
        // 不需要手动 join,自动等待所有线程完成
    });
    
    for handle in results {
        println!("Result: {}", handle.join().unwrap());
    }
}

异步

这也是个难点

相关推荐
Amos_Web5 小时前
Rust实战课程--网络资源监控器(初版)
前端·后端·rust
WujieLi21 小时前
初识 Vite+:一文了解 Rust 驱动的新一代前端工具链
javascript·rust·vite
std860211 天前
Rust 与 Python – 这是未来的语言吗?
开发语言·python·rust
std78791 天前
Rust 与 Go – 比较以及每个如何满足您的需求
开发语言·golang·rust
Amos_Web2 天前
Rust实战教程--文件管理命令行工具
前端·rust·全栈
alwaysrun2 天前
Rust中字符串与格式化
rust·display·格式化·string·str·精度
魔镜前的帅比2 天前
(开源项目)XSUN_DESKTOP_PET 2 (桌面宠物)
rust·宠物·tauri2
0110_10242 天前
tauri + rust的环境搭建---初始化以及构建
开发语言·后端·rust
像风一样自由20202 天前
Rust Tokio vs Go net/http:云原生与嵌入式生态选型指南
开发语言·golang·rust