Rust 学习笔记

所有权系统 (OwnerShip)

大概是门槛最高的rust特性

rust不使用GC和手动的malloc/free,在编译期就确定的内存的管理。

这一系统有三大规则。

  • 每个值有且仅有一个所有者
  • 所有者离开作用域Rust就会调用drop释放内存从而避免手动管理。
  • 在同一个时刻要么是一个可变的引用要么是多个不可变的引用,从而实现读写互斥,读读共存,在编译器就杜绝数据竞争。

储存在堆上的变量(如String)的赋值会进行所有权转移,原先的会被moved掉无法调用。

存储在栈上的基本类型(如i32)因为实现了copy trait会在实际上进行copy而不进行move,原先的能够调用。

变量创建的时候默认是不可变的,可变变量要用mut创建。

fn main() {
    // s1 获得字符串的所有权(数据存储在堆上)
    let s1 = String::from("Hello, Wordld");

    // 所有权从 s1 转移到 s2,此后 s1 不再有效
    let s2 = s1;

    // println!("{}", s1); // 编译错误!s1 已被 moved
    println!("{}", s2);    // 正确:s2 现在是所有者

    // 基本类型(存储在栈上)实现了 Copy trait,不会发生 move
    let x = 42;
    let y = x;  // x 仍然有效,因为 i32 实现了 Copy
    println!("x = {}, y = {}", x, y); // 都能使用

    // 不可变引用:可以同时存在多个
    let s3 = &s2; // 不可变借用
    let s4 = &s2; // 另一个不可变借用,允许
    println!("s3 = {}, s4 = {}", s3, s4);

    // 可变引用:同一时刻只能有一个
    let mut data = String::from("test");
    let r = &mut data; // 可变借用
    r.push_str("!");   // 通过可变引用修改原值
    println!("修改后: {}", r);
    // s3 和 s4 的作用域已结束,所以可以创建可变引用
}

函数要修改传入的变量也要写成可变的。

fn change(v: &mut Vec<i32>) {
    v.push(100);   // 通过 &mut 修改数据
}

let mut data = vec![1, 2, 3];
change(&mut data);

需要注意的是,Rust 语言不允许精度有损失的自动数据类型转换

常量和不可变变量的区别

变量的值可以“重新绑定”,但在“重新绑定”以前不能私自被改变,这样可以确保在每一次“绑定”之后的区域里编译器可以充分的推理程序逻辑。

模式匹配(Pattern Matching)

Rust中的match表达式的功能很强大,编译器会穷尽性的检查所有情况从而确保不会遗漏任何的分支

fn main() {
    // match 与数字匹配
    let score = 85;
    match score {
        90..=100 => println!("A"), // 范围匹配
        60..=89  => println!("B"),
        0..=59   => println!("C"),
        _ => println!("Invaild"),                // 通配符,匹配所有剩余情况
    }

    // match 解构元组
    let pair = (3, 7);
    match pair {
        (0, y) => println!("第一个是 0,第二个是 {}", y),
        (x, 0) => println!("第一个是 {},第二个是 0", x),
        (x, y) => println!("两个值: {} 和 {}", x, y),
    }
}

枚举与 Option/Result

Rust 的枚举(enum)可以携带数据,配合 match 使用能实现安全且表达力强大的控制流。

enum IpAddrKind {
    V4,
    V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

标准库中的 Option<T>Result<T, E> 是两个最核心的枚举类型,分别处理「可能为空」和「可能出错」的场景——Rust 没有 null 和异常。

use std::fs;

fn main() {
    // Option<T>: 值可能存在也可能不存在(替代 null)
    let numbers = vec![10, 20, 30];
    let first = numbers.get(0);   // 返回 Option<&i32>
    let missing = numbers.get(5); // 返回 Option<&i32>

    match first {
        Some(&val) => println!("第一个元素: {}", val),
        None => println!("没有找到"),
    }

    // 更简洁的写法:if let
    if let Some(&val) = missing {
        println!("找到: {}", val);
    } else {
        println!("索引 5 不存在");  // 预期走这个分支
    }

    // Result<T, E>: 操作可能成功也可能失败(替代异常)
    let content = fs::read_to_string("config.txt");
    match content {
        Ok(text) => println!("文件内容: {}", text),
        Err(e) => println!("读取失败: {}", e),
    }
}

这个机制就是用来实现异常处理的。

Rust 结构体

用于实现面向对象,结构体的成员叫字段。

定义时只能定义变量,实例化要使用也要用类似JSON对象的语法实例化。

struct Site {
    domain: String,
    name: String,
    nation: String,
    found: u32
}

let runoob = Site {
    domain: String::from("www.runoob.com"),
    name: String::from("RUNOOB"),
    nation: String::from("China"),
    found: 2013
};

如果正在实例化的结构体有字段名称和现存变量名称一样的,可以简化书写:

let domain = String::from("www.runoob.com");
let name = String::from("RUNOOB");
let runoob = Site {
    domain,  // 等同于 domain : domain,
    name,    // 等同于 name : name,
    nation: String::from("China"),
    traffic: 2013
};

还有可以复制部分参数的写法。

let site = Site {
    domain: String::from("www.runoob.com"),
    name: String::from("RUNOOB"),
    ..runoob
};

rust中有方便的输出整个一整个结构体实例,使用#[derive(debug)]导入调试库后在println和print宏中用{:?}(一整行)或{:#}(结构化)即可输出一整个结构体。

结构体方法

用来操纵结构体实例的函数。第一个参数必须是&self

struct Rectangle {
    width: u32,
    height: u32,
}
   
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1's area is {}", rect1.area());
}

也有不使用&self参数的结构体函数,不依赖实例但是需要声明其在哪个impl块中的。调用方式类似C++中的静态方法。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn create(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    let rect = Rectangle::create(30, 50);
    println!("{:?}", rect);
}

函数

Rust可以在一个用{}包裹的块里编写一个较为复杂的表达式,其最后的值为整个表达式的返回值,可以作为函数的返回值。

let y = {
    let x = 3;
    x + 1
};

用return也可以进行返回。

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

循环

rust没有三元语句控制循环的办法,要用while代替。

for循环用于遍历线性的数据结构。

fn main() {
    let a = [10, 20, 30, 40, 50];
    for i in a.iter() {
        println!("值为 : {}", i);
    }
}

loop循环用于实现无限循环结构,用break退出loop。

fn main() {
    let s = ['R', 'U', 'N', 'O', 'O', 'B'];
    let mut i = 0;
    loop {
        let ch = s[i];
        if ch == 'O' {
            break;
        }
        println!("\'{}\'", ch);
        i += 1;
    }
}

其本身能像return一样给外部一个返回值。

fn main() {
    let s = ['R', 'U', 'N', 'O', 'O', 'B'];
    let mut i = 0;
    let location = loop {
        let ch = s[i];
        if ch == 'O' {
            break i;
        }
        i += 1;
    };
    println!(" \'O\' 的索引为 {}", location);
}

迭代器

类似于C++的迭代器,使用next这样的操作方式迭代一个线性的集合。

对于集合,iter()返回的是不可变引用迭代器,iter_mut()返回集合的可变引用迭代器(作修改),into_iter()会将集合转移所有权并生成值迭代器,原先的失去所有权。

有如下的常见迭代器方法:

  • map():对每个元素应用给定的转换函数。
  • filter():根据给定的条件过滤集合中的元素。
  • fold():对集合中的元素进行累积处理。
  • skip():跳过指定数量的元素。
  • take():获取指定数量的元素。
  • enumerate():为每个元素提供索引。

使用map()对每个元素进行转换。

let vec = vec![1, 2, 3, 4, 5];
let squared_vec: Vec<i32> = vec.iter().map(|x| x * x).collect();

使用filter()过滤元素。

let vec = vec![1, 2, 3, 4, 5];
let filtered_vec: Vec<i32> = vec.into_iter().filter(|&x| x % 2 == 0).collect();

可以使用for迭代迭代器中的元素。

let vec = vec![1, 2, 3, 4, 5];
for &num in vec.iter() {
    println!("{}", num);
}

消耗型适配器可以使用迭代器直到其被完全消耗,可以实现另一种程度上的遍历。

  • collect():将迭代器转换为集合(如向量、哈希集)。
  • sum():计算迭代器中所有元素的和。
  • product():计算迭代器中所有元素的乘积。
  • count():返回迭代器中元素的个数。

需要注意的一点是Rust迭代器是lazy的,在调用消耗性适配器或者直接获取这些值之前是不会处理数据的。

如果需要在多线程环境中并行化操作,rayon crate 提供了并行迭代器的支持,通过 .par_iter() 代替 .iter(),可以在多线程环境中加速迭代操作。

闭包

格式let closure_name = |参数| 表达式,匿名,可以捕获外部变量,参数和返回值类型通常可以省略,可以作为变量,参数,返回值。

fn main() {
    // 闭包:省略类型注解,编译器自动推断
    let add_one = |x| x + 1;
    println!("add_one(4) = {}", add_one(4)); // 输出: 5

    // 闭包:显式标注参数和返回值类型
    let multiply = |a: i32, b: i32| -> i32 { a * b };
    println!("multiply(3, 7) = {}", multiply(3, 7)); // 输出: 21

    // 多行闭包需要加大括号
    let greet = |name: &str| {
        let greeting = format!("Hello, {}!", name);
        greeting
    };
    println!("{}", greet("runoob")); // 输出: Hello, runoob!
}

闭包可以通过三种方式捕获外部变量:

  • 按引用捕获 类似&T,外部仍然可以使用
  • 可变借用捕获 类似 &mut T,闭包需要修改变量的情况,闭包本身必须声明为mut
  • 按值捕获 闭包体消费了变量,比如把变量传进别的函数、或者作为返回值返回

这些编译器都会自动进行推断,但是当闭包体本身只用借用就能满足时,编译器不会自动 move。需要强制转移时(典型场景:把闭包传给新线程,线程可能比变量活得久,借用不安全),用 move 关键字,闭包会获取捕获变量的所有权。

fn main() {
    let owned = String::from("RUNOOB");
    // move 将 owned 的所有权转移进闭包
    let take_owned = move || println!("owned = {}", owned);
    take_owned(); // 输出: owned = RUNOOB
    // 若取消下面这行注释,将编译报错:owned 的所有权已被移入闭包
    // println!("{}", owned);
}

闭包可以作为函数参数传入,也可以作为函数的返回值返回。

作为参数要使用泛型约束指定闭包的trait类型。

// 定义一个函数,接受闭包作为参数
fn apply<F>(val: i32, f: F) -> i32
where
    F: Fn(i32) -> i32, // F 必须实现 Fn(i32) -> i32
{
    f(val)
}

fn main() {
    let double = |x| x * 2;
    let result = apply(5, double);
    println!("Result: {}", result); // 输出: Result: 10

    // 也可以直接传入匿名闭包
    let result2 = apply(3, |x| x + 100);
    println!("Result2: {}", result2); // 输出: Result2: 103
}

作为返回值是需要使用impl Trait或者Box<dyn Trait>描述返回类型。

若返回的闭包类型在编译期可以确定时,使用Impl Fn即可。

// 返回一个闭包,捕获参数 x 并与传入值相加
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    // 必须 move,否则 x 是局部引用,闭包返回后 x 已失效
    move |y| x + y
}

fn main() {
    let add_five = make_adder(5);
    println!("5 + 3 = {}", add_five(3)); // 输出: 5 + 3 = 8

    let add_ten = make_adder(10);
    println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}

需要在运行时选择不同闭包返回的时候可以使用Box<dyn Fb>将闭包分配到堆上。

fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

fn main() {
    let add_ten = make_adder(10);
    println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}

闭包的生命周期与它所捕获的变量密切相关。Rust 的生命周期系统确保闭包不会比它捕获的任何变量活得更长——如果闭包引用了一个局部变量,编译器会在编译期阻止你将闭包返回到该变量的作用域之外。

切片(Slice)

数据上的部分引用,格式是[l..r],常用的如字符串切片,本质是存储ptr和len,头尾可以省略默认表示开头和结尾。

fn main() {
    let s = String::from("broadcast");

    let part1 = &s[0..5];
    let part2 = &s[5..9];

    println!("{}={}+{}", s, part1, part2);
}

字符串切片就是核心类型str,常常以引用形式&str出现,用双引号包裹的都是 &str。

String是一个功能更加完善的字符串,其切片的结果还是&str。

枚举类

普通用法如下。

#[derive(Debug)]

enum Book {
    Papery, Electronic
}

fn main() {
    let book = Book::Papery;
    println!("{:?}", book);
}

可以添加元组属性描述。

enum Book {
    Papery(u32),
    Electronic(String),
}

let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));

如果想要命名要使用结构体语法。

enum Book {
    Papery { index: u32 },
    Electronic { url: String },
}
let book = Book::Papery{index: 1001};

用match才能访问枚举类绑定的属性。

fn main() {
    enum Book {
        Papery {index: u32},
        Electronic {url: String},
    }
   
    let book = Book::Papery{index: 1001};
    let ebook = Book::Electronic{url: String::from("url...")};
   
    match book {
        Book::Papery { index } => {
            println!("Papery book {}", index);
        },
        Book::Electronic { url } => {
            println!("E-book {}", url);
        }
    }
}

match本身可以被当作函数表达式对待,拥有返回值,但是所有分支返回的类型需要相同。

如果把枚举类附加属性定义成元组,在 match 块中需要临时指定一个名字:

enum Book {
    Papery(u32),
    Electronic {url: String},
}
let book = Book::Papery(1001);

match book {
    Book::Papery(i) => {
        println!("{}", i);
    },
    Book::Electronic { url } => {
        println!("{}", url);
    }
}

注意match一定要注意例外情况,用_表示。

rust 组织管理

Rust的代码组织概念有三个:箱、包、模块

箱 (Crate)

“箱”是二进制程序文件或者库文件,存在于“包”中,其是树状结构的,树根是编译器开始运行时编译的源文件所编译的程序。

包 (package)

当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。

一个包最多包含一个库“箱”,可以包含任意数量的二进制“箱”,但是至少包含一个“箱”(不管是库还是二进制“箱”)。

当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。

模块 (Module)

对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。

这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。

mod nation {
    mod government {
        fn govern() {}
    }
    mod congress {
        fn legislate() {}
    }
    mod court {
        fn judicial() {}
    }
}

Rust路径分隔符为::,绝对路径要从crate开始描述,相对路径从self或者super关键字或者一个标识符开始描述。

crate::nation::government::govern(); //绝对路径
nation::government::govern(); //相对路径

默认情况下的成员访问权是私有的,想要公共需要在成员定义前添加pub关键字。对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。

如果模块中定义了结构体,结构体除了其本身是私有的以外,其字段也默认是私有的。所以如果想使用模块中的结构体以及其字段,需要 pub 声明。枚举类枚举项可以内含字段,但不具备类似的性质,不需要pub声明。

Rust文件本身可以视作一个隐藏的模块。以下的代码是可以通过编译的:

// main.rs
mod second_module;

fn main() {
    println!("This is the main module.");
    println!("{}", second_module::message());
}

// second_module.rs
pub fn message() -> String {
    String::from("This is the 2nd module.")
}

use 关键字能将模块标识符引入当前的作用域,可以使用as添加别名。

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub fn govern() {}
}
   
use crate::nation::government::govern;
use crate::nation::govern as nation_govern;

fn main() {
    nation_govern();
    govern();
}

use 本身也可以与pub关键字配合使用。

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub use government::govern;
}

fn main() {
    nation::govern();
}

Rust 官方标准库字典:https://doc.rust-lang.org/stable/std/all.html

所有的系统库模块都是被默认导入的,所以在使用的时候只需要使用 use 关键字简化路径就可以方便的使用了。

Rust 错误处理

错误分为两种:

  • 不可恢复错误:逻辑出现了严重的问题,使用panic!宏可以终止这个程序。
  • 可恢复错误:操作可能失败但是可以处理,使用Result<T,E>进行枚举。

不可恢复错误

panic!宏调用时会:

  • 打印错误信息(参数)和发生位置
  • 展开(unwind)调用栈并清理资源
  • 终止程序

在cargo run前面设置RUST_BACKTRACE=1可以看到完整的调用栈从而进行debug。当panic发生在深层调用时非常有用

# Linux / macOS
RUST_BACKTRACE=1 cargo run

可恢复错误

enum Result<T,E>; {
    Ok(T),    // 操作成功,包含结果值
    Err(E),   // 操作失败,包含错误信息
}

使用match或者if let进行枚举,前面已经提到过了。

如果确定操作不会失败或者在原型阶段不想处理错误,可以偷懒,调用如下两个函数

  • .unwarp(),成功会返回T,失败会直接panic,使用默认的错误信息。
  • .expect("msg"),行为相同,但是panic信息是自定义的。

错误传播

在实际开发中函数遇到错误时不一定要自己处理,往往是将错误传播给调用者,rust提供了?运算符简化操作。

// 简化前
use std::fs::File;
use std::io::{self, Read};

// 手动传播错误(繁琐)
fn read_file_manual(path: &str) -> Result<String, io::Error> {
    let f = File::open(path);

    // 打开失败则返回 Err
    let mut file = match f {
        Ok(file) => file,
        Err(e) => return Err(e),  // 提前返回错误
    };

    let mut content = String::new();

    // 读取失败则返回 Err
    match file.read_to_string(&mut content) {
        Ok(_) => Ok(content),
        Err(e) => Err(e),
    }
}

//简化后
use std::fs::File;
use std::io::{self, Read};

// 使用 ? 运算符(简洁)
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;   // 失败自动返回 Err
    let mut content = String::new();
    file.read_to_string(&mut content)?; // 失败自动返回 Err
    Ok(content)
}

? 运算符只能用在返回 Result(或 Option)的函数中。从 Rust 1.39 起,main 函数也可以返回 Result。

错误类型分类处理

在实际项目中,你通常需要根据不同的错误类型做不同的处理。Rust 通过 kind() 方法实现这一点:

use std::fs::File;
use std::io::{self, Read};

// 将文件读取封装为独立函数,用 ? 传播错误
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    match read_text_from_file("hello.txt") {
        Ok(content) => println!("文件内容:\n{}", content),
        Err(e) => {
            // 根据错误类型做不同处理
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!("文件不存在,请检查路径");
                }
                io::ErrorKind::PermissionDenied => {
                    println!("没有权限读取该文件");
                }
                _ => {
                    println!("读取文件时发生错误: {}", e);
                }
            }
        }
    }
}

自定义错误类型

use std::fmt;
use std::num::ParseIntError;

// 定义自定义错误枚举
#[derive(Debug)]
enum AppError {
    IoError(std::io::Error),
    ParseError(ParseIntError),
    CustomError(String),
}

// 实现 Display trait,用于格式化输出
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::IoError(e) => write!(f, "IO 错误: {}", e),
            AppError::ParseError(e) => write!(f, "解析错误: {}", e),
            AppError::CustomError(msg) => write!(f, "业务错误: {}", msg),
        }
    }
}

// 实现 From trait,让 ? 运算符自动转换错误类型
impl From<std::io::Error> for AppError {
    fn from(error: std::io::Error) -> Self {
        AppError::IoError(error)
    }
}

impl From<ParseIntError> for AppError {
    fn from(error: ParseIntError) -> Self {
        AppError::ParseError(error)
    }
}

// 现在可以在同一个函数中用 ? 处理不同类型的错误
fn process_config(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?; // io::Error → AppError
    let value: i32 = content.trim().parse()?;      // ParseIntError → AppError

    if value < 0 {
        return Err(AppError::CustomError("配置值不能为负数".into()));
    }

    Ok(value)
}

fn main() {
    match process_config("config.txt") {
        Ok(val) => println!("配置值: {}", val),
        Err(e) => println!("错误: {}", e),
    }
}

实际项目中,可以使用 thiserror 库自动派生 Display 和 From 实现,大幅减少样板代码。对于应用层代码,anyhow 库提供了便捷的 anyhow::Result 类型,适合快速开发。

值不存在情况

用于替代null

enum Option<T>; {
    Some(T),  // 有值
    None,     // 没有值
}

泛型

函数中的泛型

fn max<T>(array: &[T]) -> T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}

结构体和枚举类时的泛型

机制基本与cpp类似

struct Point<T> {
    x: T,
    y: T
}

特性 trait

概念类似Java中的Interface

这样定义一个trait

trait Descriptive {
    fn describe(&self) -> String;
}

实现者必需有 describe(&self) -> String方法

用其实现一个结构体

impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}

与接口不同的方式是,trait本身可以定义一个默认方法,不定义方法的话对其调用trait就是默认方法。

trait Descriptive {
    fn describe(&self) -> String {
        String::from("[Object]")
    }
}

很多情况下我们需要传递一个函数做参数,例如回调函数、设置按钮事件等。在 Java 中函数必须以接口实现的类实例来传递,在 Rust 中可以通过传递特性参数来实现:

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}

任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,但是该函数只能调用这一个属性与方法。

这一特性可以用以下的等效语法实现,在有多个参数类型是特性的情况下非常实用。

fn output_two<T: Descriptive>(arg1: T, arg2: T) {
    println!("{}", arg1.describe());
    println!("{}", arg2.describe());
}

特性做类型的时候如果设计多个特性,可以使用+符号表示。

fn notify(item: impl Summary + Display)
fn notify<T: Summary + Display>(item: T)

复杂的关系可以用where关键词简化。

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug

特性也可以作为返回值

fn person() -> impl Descriptive {
    Person {
        name: String::from("Cali"),
        age: 24
    }
}

但是有一点,特性做返回值只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。比如结构体 A 与结构体 B 都实现了特性 Trait,下面这个函数就是错误的:

fn some_function(bool bl) -> impl Descriptive {
    if bl {
        return A {};
    } else {
        return B {};
    }
}

impl 功能十分强大,我们可以用它实现类的方法。但对于泛型类来说,有时我们需要区分一下它所属的泛型已经实现的方法来决定它接下来该实现的方法:

struct A<T> {}

impl<T: B + C> A<T> {
    fn d(&self) {}
}

这段代码声明了 A<T> 类型必须在 T 已经实现 B 和 C 特性的前提下才能有效实现此 impl 块。

生命周期

垂悬引用:引用值在使用前被释放了。

{
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

//无法通过编译
fn longer(s1: &str, s2: &str) -> &str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}

上述的longer函数也是没法通过编译的,原因是返回值引用可能会返回过期的引用。

fn main() {
    let r;
    {
        let s1 = "rust";
        let s2 = "ecmascript";
        r = longer(s1, s2);
    }
    println!("{} is longer", r);
}

在上面的情况下,返回值本身被使用的时候已经失效了。编译器为了保证在所有情况下返回值都正常,所以不允许这样的情况通过编译。

生命周期注释

生命周期注释是描述引用生命周期的办法。

虽然这样并不能够改变引用的生命周期,但可以在合适的地方声明两个引用的生命周期一致。格式是使用单引号开头并跟着一个小写字母单词。

&i32        // 常规引用
&'a i32     // 含有生命周期注释的引用
&'a mut i32 // 可变型含有生命周期注释的引用

用生命周期注释改造longer函数使得其可以通过编译。

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}

早期 Rust 不支持生命周期自动判断,所有的生命周期必须严格声明,但主流稳定版本的 Rust 已经支持了这个功能。如下代码可以通过编译:

impl<'a> Str<'a> {
    fn get_content(&self) -> &str {
        self.content
    }
}

生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str'static所表示的生命周期从程序运行开始到程序运行结束。

文件与IO

接受命令行参数

fn main() {
    let args = std::env::args();
    println!("{:?}", args);
}

直接运行程序可以得到一个inner数组,包含唯一字符串,代表当前程序所在的位置

遍历其的方法也比较简单。

fn main() {
    let args = std::env::args();
    for arg in args {
        println!("{}", arg);
    }
}

命令行输入

std::io提供了标准输入,但是没有直接读取格式化数据的方法,可能要使用字符串识别函数处理数据。

use std::io::stdin;

fn main() {
let mut str_buf = String::new();

    stdin().read_line(&mut str_buf)
        .expect("Failed to read line.");

    println!("Your input line is \n{}", str_buf);
}

文件读取

std::fs库提供了文件IO

use std::fs;

fn main() {
    let text = fs::read_to_string("./text.txt").unwrap();
    println!("{}", text);
}

如果要读取二进制文件可以用std::fs::read读取u8类型的集合。

use std::fs;

fn main() {
    let content = fs::read("./text.txt").unwrap();
    println!("{:?}", content);
}

同样也存在使用流读取的方式

use std::io::prelude::*;
use std::fs;

fn main() {
    let mut buffer = [0u8; 5];
    let mut file = fs::File::open("D:\\text.txt").unwrap();
    file.read(&mut buffer).unwrap();
    println!("{:?}", buffer);
    file.read(&mut buffer).unwrap();
    println!("{:?}", buffer);
}

文件写入

一次性写入

use std::fs;

fn main() {
    fs::write("./text.txt", "FROM RUST PROGRAM")
        .unwrap();
}

流式写入

use std::io::prelude::*;
use std::fs::File;

fn main() {
    let mut file = File::create("./text.txt").unwrap();
    file.write(b"FROM RUST PROGRAM").unwrap();
}

可以通过OpenOptions实现用特定方式打开文件。

use std::io::prelude::*;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
   
    let mut file = OpenOptions::new()
            .append(true).open("D:\\text.txt")?;

    file.write(b" APPEND WORD")?;

    Ok(())
}

这一方法还可以限制打开的权限。

use std::io::prelude::*;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
   
    let mut file = OpenOptions::new()
            .read(true).write(true).open("D:\\text.txt")?;

    file.write(b"COVER")?;

    Ok(())
}

集合与字符串

集合

集合用get返回的是Option,保证安全,如果可以确保不超出向量下标取值范围也可以使用取值语法。

遍历向量

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
            println!("{}", i);
    }
}

字符串

rust的字符串本身支持包含UTF-8字符的字符串。

可以使用+拼接。

len方法统计的是数据数据长度,使用取字符集合的方式可以正确计算字符串长度。

let s = "你好";
let len = s.len(); // 6, UTF-8每个字符3字节

let s = "hello你好";
let len = s.chars().count(); // 7

也可以用字符遍历字符串.

fn main() {
    let s = String::from("hello中文");
    for c in s.chars() {
        println!("{}", c);
    }
}

取单个字符。

fn main() {
    let s = String::from("EN中文");
    let a = s.chars().nth(2);
    println!("{:?}", a);
}

映射表

hashmap

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("color", "red");
    map.insert("size", "10 m^2");

    println!("{}", map.get("color").unwrap());
}

也可以用迭代器访问。

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("color", "red");
    map.insert("size", "10 m^2");

    for p in map.iter() {
        println!("{:?}", p);
    }

如果想要确认当前不存在才插入可以map.entry("color").or_insert("red");,表示如果没有键为 “color” 的键值对就添加它并设定值为 “red”,否则将跳过。

并发编程

通过std::thread::spawn创建新线程

use std::thread;
use std::time::Duration;

fn spawn_function() {
    for i in 0..5 {
        println!("spawned thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

fn main() {
    thread::spawn(spawn_function);

    for i in 0..3 {
        println!("main thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

/*
输出
main thread print 0
spawned thread print 0
main thread print 1
spawned thread print 1
main thread print 2
spawned thread print 2
*/

可以看出主线程结束后会结束创建的线程

spawn的参数是一个无参数的函数,可以传递闭包。

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 0..5 {
            println!("spawned thread print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 0..3 {
        println!("main thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

spawn的返回值是可以控制线程,使用join方法可以让子线程运行结束后再停止运行程序。

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("spawned thread print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 0..3 {
        println!("main thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

可以使用闭包的move关键字来让子线程使用当前函数中的资源。

use std::thread;

fn main() {
    let s = "hello";
   
    let handle = thread::spawn(move || {
        println!("{}", s);
    });

    handle.join().unwrap();
}

和go一样使用channel实现消息传递并发,由transmitter和receiver组成。

std::sync::mpsc 包含了消息传递的方法:

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

可以进行自定义的语法扩展,主要有声明式宏(Declarative Macros)和过程宏(Procedural Macros),主要是声明式宏。

使用macro_rules!定义声明式宏。

macro_rules! my_macro {
    // 模式匹配和展开
    ($arg:expr) => {
        // 生成的代码
        // 使用 $arg 来代替匹配到的表达式
    };
}

示例

// 宏的定义
macro_rules! greet {
    // 模式匹配
    ($name:expr) => {
        // 宏的展开
        println!("Hello, {}!", $name);
    };
}

fn main() {
    // 调用宏
    greet!("World");
}

vec!宏,

// 宏的定义
macro_rules! vec {
    // 基本情况,空的情况
    () => {
        Vec::new()
    };
   
    // 递归情况,带有元素的情况
    ($($element:expr),+ $(,)?) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($element);
            )+
            temp_vec
        }
    };
}

fn main() {
    // 调用宏
    let my_vec = vec![1, 2, 3];
    println!("{:?}", my_vec); // 输出: [1, 2, 3]

    let empty_vec = vec![];
    println!("{:?}", empty_vec); // 输出: []
}

在这个例子中,vec! 宏使用了模式匹配,以及 $($element:expr),+ $(,)? 这样的语法来捕获传递给宏的元素,并用它们创建一个 Vec。

注意,$(,)? 用于处理末尾的逗号,使得在不同的使用情境下都能正常工作。

过程宏是一种更为灵活和强大的宏,允许在编译时通过自定义代码生成过程来操作抽象语法树(AST)。过程宏在功能上更接近于函数,但是它们在编写和使用上更加复杂。

分为派生宏和属性宏,通常需要使用proc_macro库提供的功能。

智能指针

智能指针和使用场景的对应情况如下。

  • 当需要在堆上分配内存时,使用 Box<T>
  • 当需要多处共享所有权时,使用 Rc<T>Arc<T>
  • 当需要内部可变性时,使用 RefCell<T>
  • 当需要线程安全的共享所有权时,使用 Arc<T>
  • 当需要互斥访问数据时,使用 Mutex<T>
  • 当需要读取-写入访问数据时,使用 RwLock<T>
  • 当需要解决循环引用问题时,使用 Weak<T>

Box<T>

只是在堆上分配一块内存,并将值存储在内存上

Rc<T>

Rc<T>(引用计数指针)允许多个所有者共享数据,它使用引用计数来跟踪数据的所有者数量,并在所有者数量为零时释放数据。

Rc<T> 适用于单线程环境下的数据共享。

use std::rc::Rc;

let data = Rc::new(5);
let data_clone = Rc::clone(&data);

Arc<T>

Arc<T>(原子引用计数指针)与 Rc<T> 类似,但是可以安全地在多线程环境中共享数据,因为它使用原子操作来更新引用计数。

use std::sync::Arc;

let data = Arc::new(5);
let data_clone = Arc::clone(&data);

RefCell<T>

允许在运行时检查借用规则,利用内部可变性提供了一种安全的内部可变性模式,允许在不可变引用的情况下修改数据。只能用于单线程环境

use std::cell::RefCell;

let data = RefCell::new(5);
let mut borrowed_data = data.borrow_mut();
*borrowed_data = 10;

可以和Rc搭配,使得多个所有者共享同一份数据并且同时允许修改。

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let counter = Rc::new(RefCell::new(0));

    let a = Rc::clone(&counter);
    let b = Rc::clone(&counter);

    *a.borrow_mut() += 1;
    *b.borrow_mut() += 2;

    println!("{}", counter.borrow()); // 3
}

Mutex<T>

互斥锁,确保只有一个线程可以访问Mutex内部数据,有lock和unlock方法。

use std::sync::Mutex;

let m = Mutex::new(5);
let mut data = m.lock().unwrap();

RwLock<T>

一种读取-写入锁,允许多个读取者同时访问数据,但在写入时是排他的。

锁被持有期间通过guard访问数据

示例

use std::sync::RwLock;

fn main() {
    let data = RwLock::new(vec![1, 2, 3]);

    // 读锁:可以同时存在多个
    {
        let values = data.read().unwrap();
        println!("{values:?}");
    }

    // 写锁:独占访问
    {
        let mut values = data.write().unwrap();
        values.push(4);
    }

    println!("{:?}", data.read().unwrap());
}

常见多线程写法Arc<RwLock<T>>

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2,
    3]));

    let reader_data = Arc::clone(&data);
    let reader = thread::spawn(move || {
        let values = reader_data.read().unwrap();
        println!("读取: {values:?}");
    });

    let writer_data = Arc::clone(&data);
    let writer = thread::spawn(move || {
        let mut values =
        writer_data.write().unwrap();
        values.push(4);
    });

    reader.join().unwrap();
    writer.join().unwrap();

    println!("最终结果: {:?}",
    data.read().unwrap());
}

Weak<T>

Rc<T>的非拥有智能指针,不增加引用计数,解决循环引用问题。

use std::rc::{Rc, Weak};

let five = Rc::new(5);
let weak_five = Rc::downgrade(&five);

智能指针的生命周期管理

智能指针可以帮助管理数据的生命周期,当智能指针被销毁时,它们会自动释放内存,从而避免了内存泄漏和野指针的问题。

此外,智能指针还允许在创建时指定特定的析构函数,以实现自定义的资源管理

智能指针是 Rust 安全性模型的重要组成部分,允许开发者编写低级代码而不必担心内存安全问题。

通过智能指针,Rust 既保持了 C 语言的控制能力,又避免了其风险。

异步编程async/await

异步编程允许程序在等待 I/O 操作(如文件读写、网络通信等)时不被阻塞,从而提高性能和响应性。

Future:Future 是 Rust 中表示异步操作的抽象。它是一个可能还没有完成的计算,将来某个时刻会返回一个值或一个错误。

async/await:async 关键字用于定义一个异步函数,它返回一个 Future。await 关键字用于暂停当前 Future 的执行,直到它完成。

示例

// 引入所需的依赖库
use tokio;
use tokio::time::{self, Duration};

// 异步函数,模拟异步任务
async fn async_task() -> u32 {
    // 模拟异步操作,等待 1 秒钟
    time::sleep(Duration::from_secs(1)).await;
    // 返回结果
    42
}

// 异步任务执行函数
async fn execute_async_task() {
    // 调用异步任务,并等待其完成
    let result = async_task().await;
    // 输出结果
    println!("Async task result: {}", result);
}

// 主函数
#[tokio::main]
async fn main() {
    println!("Start executing async task...");
    // 调用异步任务执行函数,并等待其完成
    execute_async_task().await;
    println!("Async task completed!");
}

以上代码中,我们首先定义了一个异步函数 async_task(),该函数模拟了一个异步操作,使用 tokio::time::delay_for() 方法来等待 1 秒钟,然后返回结果 42。接着定义了一个异步任务执行函数 execute_async_task(),在其中调用了异步函数,并使用 await 关键字等待异步任务的完成。最后在 main 函数中使用 tokio::main 宏来运行异步任务执行函数,并等待其完成。

运行该程序,可以看到程序输出了开始执行异步任务的提示,然后等待了 1 秒钟后输出了异步任务的结果,并最终输出了异步任务完成的提示.

Start executing async task...
Async task result: 42
Async task completed!

async

用于定义异步函数,返回Future或impl Future。异步函数执行时会返回一个未完成的Future对象,表示一个尚未完成的计算或操作。异步函数本身可以包含await表达式,用于等待其他异步操作的完成。

await

await 关键字用于等待异步操作的完成,并获取其结果。

await 表达式只能在异步函数或异步块中使用,它会暂停当前的异步函数执行,等待被等待的 Future 完成,然后继续执行后续的代码。

异步函数的返回值类型通常是 impl Future<Output = T>,其中 T 是异步操作的结果类型。由于异步函数的返回值是一个 Future,因此可以使用 .await 来等待异步操作的完成,并获取其结果。

异步块

可以在同步代码中使用异步操作。异步块由 async { } 构成,其中可以包含异步函数调用和 await 表达式。

async {
    let result1 = hello().await;
    let result2 = add(1, 2).await;
    println!("Result: {}, {}", result1, result2);
};

异步任务执行

在 Rust 中,异步任务通常需要在执行上下文中运行,可以使用 tokio::main、async-std 的 task::block_on 或 futures::executor::block_on 等函数来执行异步任务。这些函数会接受一个异步函数或异步块,并在当前线程或执行环境中执行它。

use async_std::task;

fn main() {
    task::block_on(print_hello());
}

错误处理

await 后面跟一个 ? 操作符可以传播错误。如果 await 的 Future 完成时返回了一个错误,那么这个错误会被传播到调用者。

async fn my_async_function() -> Result<(), MyError> {
    some_async_operation().await?;
    // 如果 some_async_operation 出错,错误会被传播
}

异步trait

Rust 允许为 trait 定义异步方法。这使得你可以为不同类型的对象定义异步操作。

trait MyAsyncTrait {
    async fn async_method(&self) -> Result<(), MyError>;
}

impl MyAsyncTrait for MyType {
    async fn async_method(&self) -> Result<(), MyError> {
        // 异步逻辑
    }
}

异步上下文

在 Rust 中,异步代码通常在异步运行时(如 Tokio 或 async-std)中执行。这些运行时提供了调度和执行异步任务的机制。

#[tokio::main]
async fn main() {
    some_async_operation().await;
}

异步宏

Rust 提供了一些异步宏,如 tokio::spawn,用于在异步运行时中启动新的异步任务。

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // 异步逻辑
    });
    handle.await.unwrap();
}

异步I/O

Rust 的标准库提供了异步 I/O 操作,如 tokio::fs::File 和 async_std::fs::File。

use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut file = File::open("file.txt").await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    println!("Contents: {}", contents);
    Ok(())
}

异步通道

Rust 的一些异步运行时提供了异步通道(如 tokio::sync::mpsc),允许在异步任务之间传递消息。

use tokio::sync::mpsc;
use tokio::spawn;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);

    let child = spawn(async move {
        let response = "Hello, world!".to_string();
        tx.send(response).await.unwrap();
    });

    let response = rx.recv().await.unwrap();
    println!("Received: {}", response);

    child.await.unwrap();
}