前言

  _                           _               _____           _   
 | |                         (_)             |  __ \         | |  
 | |     ___  __ _ _ __ _ __  _ _ __   __ _  | |__) |   _ ___| |_ 
 | |    / _ \/ _` | '__| '_ \| | '_ \ / _` | |  _  / | | / __| __|
 | |___|  __/ (_| | |  | | | | | | | | (_| | | | \ \ |_| \__ \ |_ 
 |______\___|\__,_|_|  |_| |_|_|_| |_|\__, | |_|  \_\__,_|___/\__|
                                       __/ |                      
                                      |___/                       

Rust 是一个系统级编程语言。一门赋予每个人构建可靠且高效软件能力的语言。

Rust 语言发展简史

起源与早期阶段(2006–2015)

2006年:Rust 由 Mozilla 工程师 Graydon Hoare 发起,初衷是解决 C++ 在内存安全和并发编程中的痛点(如电梯软件崩溃事件启发)。

2010年:首个公开版本 0.1 发布,展示基础语法;同年 Mozilla 正式资助开发,用于重构 Firefox 的 CSS 多线程解析模块。

2012年:0.3 版本引入所有权系统的雏形,奠定内存安全基础。

2015年5月:里程碑版本 1.0 发布,标志着语言稳定性和工业级可用性,吸引开发者广泛关注。

生态扩展与成熟(2016–2020)

2016年:1.6 版本引入 libcore,支持嵌入式开发。

2017年:1.15 版本支持派生宏,减少样板代码。

2019年:1.34 版本稳定支持 async/await,推动异步编程发展。

2020年:Mozilla 裁员引发 Rust 社区危机,但同年成立 Rust 基金会(由 AWS、谷歌、微软等支持),确保项目独立性。

现代发展与未来规划(2021–2024)

2021年:引入 const 泛型,提升编译时能力。

2024年:计划发布 Rust 2024 Edition,改进语法一致性、异步生成器支持(gen 关键字)等。

Rust 的优势

高性能

Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。

可靠性

Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。

生产力

Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具——包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。

你好 Rust

安装

第一步是 Rust 安装,我们将通过 rustup 来安装 Rust。

通过 rustup 安装

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

如果安装成功将出现下面这行

Rust is installed now. Great!

更新

rustup update

卸载

rustup self uninstall

编写程序

使用 cargo 创建项目

cargo new hello_world
cd hello_world

构建并运行

构建程序

cargo build

输出

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s

执行程序

./target/debug/hello_world

输出

Hello, world!

发布构建

当项目最终准备好发布时,可以使用 cargo build --release 来优化编译项目。 这会在 target/release 而不是 target/debug 下生成可执行文件。

cargo build --release

线上执行程序

./target/release/hello_world

输出

Hello, world!

cargo run 构建并运行

cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/hello_world`

输出

Hello, world!

基础语法

变量

变量是用于管理和存储数据空间。变量可以用来表示值、表达式或指向数据的引用。

变量三要素

  • 名称: 变量的名称是唯一标识符,代表它所存储的数据。
  • 类型: 变量的类型决定了其存储的数据类型,例如整数、字符串、布尔值等。
  • 值: 变量的值是其实际存储的数据。

可变与不可变

Rust 声明的变量默认是不可变的。只能读取不能进行赋值。

代码 1: 不可变变量

fn main() {
    let x: i32 = 10; // 声明不可变变量
    println!("{}", x); // 10
    // x = 20; // 为不可变变量赋值-编译错误
}

可变变量需要使用 let mut 声明,可变变量即可以读取也可以赋值。

代码 2: 可变变量

fn main() {
    let mut x: i32 = 10; // 可变变量
    println!("{}", x); // 10
    x = 20; // 为可变变量赋值
    println!("{}", x); // 20
}

变量隐藏

我们可以定义一个与之前同名的新变量,第一个变量被第二个隐藏了。这意味着当你使用变量的名称时,编译器将看到第二个变量。

代码 3: 变量隐藏

fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x + 2;
        println!("The value of x in inner scope is : {}", x); // 8
    }
    println!("The value of x : {}", x); // 6
}

常量

常量是绑定到一个名称的不允许改变的值,声明常量使用 const,并且必须注明值类型。常量总是不可变的。常量只能被设置为常量表达式。

常量命名规范:大写字母加下划线

代码 4: 常量

fn main() {
    const THREE_HOUR_IN_SECONDS: u32 = 3 * 60 * 60;
    println!("{}", THREE_HOUR_IN_SECONDS); // 10800
}

数据类型

每一个值都属于某一个数据类型,指明何种数据以便明确数据处理方式。数据类型可以分为:标量(scalar)和复合类型(compound)。

Rust 是静态类型语言,在编译时必须知道所有变量的类型。 根据值及其使用方式,编译器通常可以推断出我们想要的类型。

标量类型

整型

是一个没有小数部分的数字。

表格 1: Rust 中的整型

长度有符号无符号
8 biti8u8
16 biti16u16
32 biti32u32
64 biti64u64
128 biti128u128
archisizeusize

代码 5: Rust 中的整型

fn main() {
    let xi8 = 127i8;
    let xu8: u8 = 255;
    println!("xi8: {}, xu8: {}", xi8, xu8);
    let xi16 = 127i16;
    let xu16: u16 = 255;
    println!("xi16: {}, xu16: {}", xi16, xu16);
    let xi32 = 127i32;
    let xu32: u32 = 255;
    println!("xi32: {}, xu32: {}", xi32, xu32);
    let xi64 = 127i64;
    let xu64: u64 = 25_5;
    println!("xi64: {}, xu64: {}", xi64, xu64);
    let xi128 = 127i128;
    let xu128: u128 = 100_100;
    println!("xi128: {}, xu128: {}", xi128, xu128);
    let x_isize = 127isize;
    let x_usize: usize = 255;
    println!("xisize: {}, xusize: {}", x_isize, x_usize);
}

表格 2: Rust 中整型字面值

数字字面值例子
Decimal(十进制)98_222
Hex(十六进制)0xff
Octal(八进制)0o77
Binary(二进制)0b1111_0000
Byte(单字节字符)b’A’

可以使用类型后缀来指定类型,如 3u32

代码 6: 整型字面值

fn main() {
    let x_decimal = 98_222;
    println!("x_decimal: {}", x_decimal);
    let x_hex = 0xff;
    println!("x_hex: {}", x_hex);
    let x_octal = 0o77;
    println!("x_octal: {}", x_octal);
    let x_binary = 0b1111_0000;
    println!("x_binary: {}", x_binary);
    let x_byte  = b'A';
    println!("x_byte: {}", x_byte);

}

浮点型

浮点数是带小数点的数字。浮点数类型为 f32f64。默认是 f64

代码 7: 浮点型

fn main() {
    let f = 2.0;
    println!("f {}", f);
    let f:f32 = 3.0;
    println!("f {}", f);
}

布尔型

布尔类型用来表示真和假,使用 bool 关键字,只有两个肯能的值 truefalse

代码 8: 布尔型

fn main() {
    let t = true;
    println!("t {}", t);
    let t: bool = false;
    println!("t {}", t);
}

字符型

字符类型是语言中最原生的字母类型,用关键字 char 表示。char 字面量用单引号声明。

代码 9: 字符型

fn main() {
    let c = 'z';
    let z: char ='ℤ'; // with
    let heart_eyed_cat ='😻'
}

复合类型

将多个值组合成一个复合类型。Rust 有两个原生的复合类型,元组(tuple)和数组(array)。

元组

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定,一旦声明其长度不会增大或缩小。

使用在圆括号中的逗号分隔的值列表来创建一个元组。元组的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。

代码 10: 元组

fn main() {
    // 元组
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    println!("{} {} {}", tup.0, tup.1, tup.2);

    // 使用模式匹配来解构元组
    let (x, y, z) = tup;
    println!("{} {} {}", x, y, z);
}

不带任何值的元组被称为 单元(unit) 元组。这种值以及对应的值都写做 ()。表示空值或者空的返回类型。如果表达式不返回其他任何值,则会隐式返回单元值。

数组

数组可以包含相同类型的多个值,数组的长度是固定的。

可以像这样来编写数组类型 [i32; 5] ,在方括号中包含每个元素的类型,后跟分号,再跟数组元素的数量。

可以像这样来创建数组的值 [1, 2, 3],使用在方括号中用逗号分隔的值列表来创建一个数组。还可以使用 [3; 5] 来创建每个元素都为相同值的数组。


函数

fn 后面跟着函数名和一对圆括号来定义函数。大括号告诉编译器哪里式函数的开始和结尾。使用函数名后跟圆括号来调用任意函数。

有参数的函数,参数是特殊变量。是函数签名的一部分。当参数拥有形参时,可以为这些参数提供具体的值实参。

函数体由一系列的语句和一个可选的结尾表达式构成。 语句:执行一些操作但不返回值的指令。 表达式:计算并产生一个值。

返回值:

无需对返回值进行命名,但是需要在 -> 后声明返回值的类型。函数的返回值等同于最后一个表达式的值。使用 return 和指定值,可以从函数中提前返回。

fn main() {
    println!("Hello, world!");
    another_function();
    another_function_x(5);
    print_labeled_measurement(5, 'h');
    println!("The value of x is: {}", five());
}

// 无参数函数
fn another_function() {
    println!("Another function.");
}

// 单参数函数
fn another_function_x(x: i32) {
    println!("The value of x is: {}", x)
}

// 多参数函数
fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {} {}", value, unit_label);
}

// 具有返回值的函数
fn five() -> i32 {
    5
}

// 具有返回值的函数
fn five() -> i32 {
    return 5
}

控制流

根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码的能力。

if 表达式

允许根据条件执行不同的代码分支。if 只会执行第一个分支为 true 的代码

fn main() {
    let number = 6;
    // `if` 只会执行第一个分支为 `true` 的代码
    if number % 4 == 0 {
        println!("number {} is divisible by 4", number);
    } else if number % 3 == 0 {
        println!("number {} is divisible by 3", number);
    } else if number % 2 == 0 {
        println!("number {} is divisible by 2", number);
    } else {
        println!("number {} is not divisible by 4,3, or 2", number);
    }
}

因为 if 是一个表达式,我们可以在 let 语句的右侧使用它。所有分支中的代码块都必须返回相同的类型。

fn main() {
    let condition = false;
    let number = if condition { 5 } else { 6 };
    println!("The value of number is: {number}");
}

循环

多次执行同一段代码一个循环执行循环体中的代码直到结尾并紧接着返回开头继续执行。

loop 循环表达式

fn main() {
    let mut number = 0;
    loop {
        number = number + 1;
        if number == 2 {
            continue; // 跳过本次循环
        }
        if number > 4 {
            break; // 退出所有循环
        }
        println!("number is {number}!");
    }
    println!("number loop is end!");

    println!("loop return value start!");
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {result}");

}

循环标签

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;
        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up; // 退出 'counting_up 标签
            }
            remaining -= 1;
        }
        count += 1;
    }
    println!("End count = {}", count);
}

while 条件循环

while 的表达式不为 true 时结束循环

fn main() {
    let mut number = 3;
    while number != 0 {
        println!("{number}");
        number -= 1;
    }
    println!("LIFTOFF!");
}

for 循环

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("The value is {}", element)
    }

    // 使用 Range 生成迭代器
    for i in 1..4 {
        println!("i {}", i)
    }
}

match 控制流

match 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应的代码。模式可由字面量、变量、通配符和许多其他内容构成。它的力量来源于模式的表现力及编译器的检查,它确保了所有可能的情况都得到了处理。

match 执行时会将表达式的值按顺序与每一个分支的模式进行比较,如果模式匹配了这个值,那么相关联的代码将会被执行。如果不匹配就继续检查下一个分支。每一个分支关联的是一个表达式,而表达是的结果值将作为 match 表达式的返回值。如果分支代码多的话需要使用大括号,并且逗号可以省略不写。

#![allow(unused)]

fn main() {
match 表达式 {
    模式1 => 代码,
    模式2 => 代码,
}

}

枚举举例

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_coin(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
fn main() {
    let penny = Coin::Penny;
    let nickel = Coin::Nickel;
    let dime = Coin::Dime;
    let quarter = Coin::Quarter;
    println!("penny: {}", value_in_coin(penny));
    println!("nickel: {}", value_in_coin(nickel));
    println!("dime: {}", value_in_coin(dime));
    println!("quarter: {}", value_in_coin(quarter));
    
}

绑定值模式

匹配分支的另一个有用的功能是绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。


#[derive(Debug)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
}

fn value_in_coin(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("{:?}", state);
            25
        }
    }
}

fn main() {
    let penny = Coin::Penny;
    let nickel = Coin::Nickel;
    let dime = Coin::Dime;
    let alabama_quarter = Coin::Quarter(UsState::Alabama);
    let alaska_quarter = Coin::Quarter(UsState::Alaska);
    println!("penny: {}", value_in_coin(penny));
    println!("nickel: {}", value_in_coin(nickel));
    println!("dime: {}", value_in_coin(dime));
    println!("alabama_quarter: {}", value_in_coin(alabama_quarter));
    println!("alaska_quarter: {}", value_in_coin(alaska_quarter));
}

匹配 Option<T>

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i+1)
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

    println!("{}", six.unwrap());
    if none == None {
        println!("None");
    }

}

通配符

fn main() {
    let dice_roll = 9;
    let dice = match dice_roll {
        1..=8 => 8, // 匹配一个范围
        9 => 9, // 匹配单独的值
        other => other, // 匹配所有未匹配的值
        // _ => 0, // 匹配所有未匹配的值但是不需要使用该值
    };
    println!("{}", dice);
}

if let

#![allow(unused)]
fn main() {
let x = Some(3u8)
if let Some(s) = x {
    println!("{}", x);
}
}

所有权

所有权是 Rust 最为与众不同的特性,对语言的其他部分有着深刻的意义。让 Rust 无需垃圾回收也能保障内存安全。

所有权(ownership) 是 Rust 用于如何管理内存的一组规则。所有程序都必须管理运行时使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时有规律的寻找不再使用的内存。在另一些语言中,程序员必须手动分配和释放内存。Rust 通过所有权系统管理内存。

栈与堆都是代码在运行时可供使用的内存,但是他们的结构不同。栈以放入值的顺序存储并与相反的顺序取出值。这也被称作后进先出(last in, first out)。增加数据叫进栈,取出数据叫出栈。栈中的数据都必须占用已知且固定的大小。在编译时未知或者大小可能发生变化的,需要改为存储在堆上。堆时缺乏组织的,当向堆放入数据时,内存分配器在某处找到一个足够大的空位。把它标记为已使用并返回一个该位置地址的指针。这个过程称为在堆上分配内存。

入栈为什么比在堆上分配内存快:

  1. 入栈无需搜索可用内存空间。其位置总是在栈顶。堆需要搜索内存空间,并记录。
  2. 访问堆上的数据必须通过指针来访问,现代处理器在内存中跳转的越少就越快。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。

所有权规则

  1. Rust 中每一个值都有一个所有者(owner)。
  2. 值在任何时刻有且只有一个所有者。
  3. 当所有者离开作用域,这个值将被丢弃

变量作用域

s 进入到这个作用域时,它就是有效的。一直持续到它离开作用域为止。

fn main() {
    { // 变量尚未声明,`s` 无效
        let s = "hello"; // 从此处起,变量`s`是有效的
        println!("{}, world!", s);
    } // 变量离开作用域,变量 `s`` 无效
}

String 与 字符串字面值。对于字符串字面值,在编译时就知道确切的大小。会直接编码进最终的可执行文件中。这使得字符串字面值快速且高效。对于 String 来说为了支持可变可增长的文本,需要在堆上分配一个编译时未知大小的内容来存放内容。

  • 必须在运行时向分配器请求内存。
  • 当我们使用完 String 时将内存返还给分配起的方法。

String::from 在运行时申请内存。当变量离开作用域的时候 Rust 为我们调用一个特殊的函数 drop。在 drop 里作者可以放置释放内存的代码。

fn main() {
    let mut s = String::from("Hello");// 请求内存
    s.push_str(", world!");

    println!("{}", s);
} // 返还给分配器

变量与数据交互的方式:移动

#![allow(unused)]
fn main() {
let x = 5;
let y = x; // 生成一个 `x` 值的拷贝并绑定到 `y`
}

String 由三部分组成,ptr(字符串数据指针)、len(字符数组长度)、capacity(字符数组容量)。字符串的数据存储在堆中。我们将 s1 的值赋值给 s2String 的数据被复制了,我们拷贝了 ptrlencapacity,但是没有拷贝堆中的数据。s1 的变量被无效了,这个操作被称为移动。

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

变量与数据交互的方式:克隆

如果想要拷贝堆中的数据需要使用 s1.clone(),只在栈上的数据会拷贝而不需要克隆。

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1;
    // println!("{}", s1); s1 变量无效
    let s3 = s2.clone();
    println!("{} {}", s2, s3);
}

所有权与函数

将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或复制。就像赋值语句一样。

fn main() {
    let s = String::from("Hello World!");
    takes_ownership(s); // 此行过后 s 变量已经被无效
    // println!("{}", s); // 编译错误: borrow of moved value: `s` value borrowed here after move
    let x = 5;
    makes_copy(x);
    println!("{}", x);
}
fn takes_ownership(ss: String) {
    println!("{}", ss);
} // 调用 ss.drop 释放内存

fn makes_copy(y: i32) {
    println!("{}", y);
} // 这里 y 移出作用域,没有特殊之处。

返回值与作用域

fn main() {
    let s1 = gives_ownership();
    println!("{s1}");
    let s2 = String::from("hello"); // s2 进入作用域
    let s3 = takes_and_gives_back(s2); // s2 被移动到函数中它也将值返回值移动给 s3。
    // println!("{s2}");
    println!("{s3}");
} // s1、s3 移出作用域,s2 也移出作用域,但是已经被移走什么都不会发生。

fn gives_ownership() -> String { // 将返回值移动值调用他的函数
    let some_string = String::from("yours"); // some_string 进入作用域
    some_string // 返回 some_string 并移出给调用它的函数
}

fn takes_and_gives_back(s: String) -> String {
    s
}

引用与借用

引用(reference)像一个指针。它是一个地址,我们可以由此访问存储与其他变量的数据。与指针不同,引用确保指向指向某个特定类型的有效值。我们将创建引用的行为称为借用。引用只能读取其中的值,如果想要修改其中的值可以使用可变引用 &mut。有一个可变引用就不能再创建其他引用为了避免数据竞争。

悬垂引用(Dangling References),在具有指针的语言中,很容易在释放内存时保留一个指向它的指针而错误的生成一个悬垂指针。

Rust 中编译器确保引用永远不会成为悬垂引用。在未离开作用域之前内存永远是有效的。

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of {s2} is {len}");

    let len = calculate_length_ref(&s2);
    println!("The length of {s2} is {len}");

    let mut s1 = String::from("hello");
    change(&mut s1);
    println!("s1 is {s1}");

    let r1 = &mut s1;
    // let r2 = &mut s1; // cannot borrow `s1` as mutable more than once at a time second mutable borrow occurs here
    println!("{r1}")
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

fn calculate_length_ref(s: &String) -> usize {
    // s 是 String 的引用
    s.len()
} // 这里 s 离开作用域,但是它是引用并没有所有权,所以什么也不会发生。

fn change(s: &mut String) {
    s.push_str(", world");
}

Slice 类型

slice 允许引用集合中一段连续的元素,而不用引用整个集合。slice 是一种引用,所以它没有所有权。使用由s[start_index..end_index] 中,中括号指定的 range 创建一个 slicestart_indexslice 的第一个位置。end_index 则是最后一个位置的后一个值。在 slice 内部存储了开始位置和长度。省略 start_index 则表示 0,省略 end_index 表示最后一个元素。

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];
    println!("{hello} {world}")
}

字符串字面值:&str 它是一个指向二进制程序位置的 slice。这也就是为什么字符串字面值是不可变的。&str 是一个不可变引用。

结构体

struct 是一个自定义数据类型,允许包装和命名多个相关的值。从而形成一个有意义的组合。

结构体简写语法:变量名如果与结构体字段名相同,可以省略字段名。

// 结构体定义
#[derive(Debug)]
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // 结构体实例创建
    let mut user1 = User {
        active: true,
        username: String::from("Li Li"),
        email: String::from("lili@zhouyi.xin"),
        sign_in_count: 1,
    };

    // 结构体字段修改
    user1.email = String::from("li.li@zhouyi.xin");

    // 结构体访问
    println!("user1.active: {}", user1.active);
    println!("user1.username : {}", user1.username);
    println!("user1.email : {}", user1.email);
    println!("user1.sign_in_count : {}", user1.sign_in_count);

    let user2 = build_user(user1.email.clone(), user1.username.clone());
    println!("user2.active: {}", user2.active);
    println!("user2.username : {}", user2.username);
    println!("user2.email : {}", user2.email);
    println!("user2.sign_in_count : {}", user2.sign_in_count);
}

fn build_user(email: String, username: String) -> User {
    // 也可以作为表达式返回结构体
    User {
        active: true,
        username, // 由于参数名与字段名相同可以省略字段名。
        email,
        sign_in_count: 1,
    }
}

结构体更新语法

更新语法会转移结构体中的变量所有权

// 结构体定义
#[derive(Debug)]
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let email = String::from("li.li@zhouyi@xin");
    let username = String::from("li.li");
    let user1 = build_user(email, username);
    let user2 = User {
        username: String::from("lili"),
        ..user1
    };
    println!("user2.active: {}", user2.active);
    println!("user2.username : {}", user2.username);
    println!("user2.email : {}", user2.email);
    println!("user2.sign_in_count : {}", user2.sign_in_count);
}

fn build_user(email: String, username: String) -> User {
    // 也可以作为表达式返回结构体
    User {
        active: true,
        username, // 由于参数名与字段名相同可以省略字段名。
        email,
        sign_in_count: 1,
    }
}

没有命名字段的元组结构体

元组结构体有着结构体名称提供的含义,但没有具体的字段名称。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时。

类单元结构体

没有任何字段的结构体,类似于 (),使用场景想在某个类型上实现 trait, 但不需要在类型中存储数据。

#[derive(Debug)]
struct SayHello;
trait Hello {
    fn hello();
}
impl Hello for SayHello{
    fn hello() {
        println!("hello");
    }
}
fn main() {
    let subject = SayHello;
    println!("{:?}", subject);
    SayHello::hello();
}

方法

方法与函数类似,它使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数不同的是,它们在结构体(枚举或 trait 对象)的上下文中被定义。并且它们第一个参数名总是 self。它代表调用该方法的结构体实例。

&selfself: &Self 的缩写 Selfimpl 类型的别名。self 参数支持所有借用规则,可以转移所有权,可以不可变借用,也可以可变借用。

fn rect_var() {
    let width1 = 30;
    let height1 = 20;
    println!("The area of Rectangles is {}", area(width1, height1));
}
fn area(width: u32, height: u32) -> u32 {
    return width * height;
}

fn rect_tuple() {
    let rect = (30, 40);
    println!("The area of Rectangles is {}", area_tuple(rect));
}

fn area_tuple(rectangles: (u32, u32)) -> u32 {
    return rectangles.0 * rectangles.1;
}

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

impl Rectangle {
    fn area(&self) -> u32 { // &self 是 self: &Self 的缩写 Self 是 impl 类型的别名
        self.height * self.width
    }

    fn area_full(self: &Self) -> u32 {
        self.height * self.width
    }
}

fn rect_struct() -> Rectangle {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!("{}", area_struct(&rect));
    println!("{:?}", rect);
    println!("{:#?}", rect);
    dbg!(rect)
}

fn area_struct(rect: &Rectangle) -> u32 {
    rect.width * rect.height
}

fn rect_method() {
    let rect = Rectangle {
        width: 20,
        height: 30,
    };

    println!("rect_method area: {}", rect.area());
    println!("rect_method area_full: {}", rect.area_full());
}

fn main() {
    rect_var();
    rect_tuple();
    rect_struct();
    rect_method();
}

关联函数

所有在 impl 块中定义的函数被称为关联函数,因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一个参数的关联函数。因为它们并不作用一个结构体的实例。

不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new

使用结构体名和 :: 来调用这个关联函数。

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

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Self{
            width:size,
            height:size,
        }
    }
}
fn main() {
    println!("square is {:#?}", Rectangle::square(2));
}

每个结构体都可以拥有多个 impl 块。

枚举

结构体用来讲不通类型的字段几数组组合在一起。枚举用来表示有限集合中的成员,必须是可以枚举出所有可能的值。

我们定义的每一个枚举成员的名字也变成了一个构建枚举的实例的函数。作为定义枚举的结果这些构造函数会被自动定义。

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

#[derive(Debug)]
enum IpAddrKind {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home_v4 = IpAddrKind::V4(127, 0, 0, 1);
    let home_v6 = IpAddrKind::V6(String::from("::1"));
    println!("{:?} {:?}", home_v4, home_v6);

    let std_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
    let std_v6 = IpAddr::V6(Ipv6Addr::new(0,0,0,0,0,0,0,1));
    println!("{:?} {:?}", std_v4, std_v6);
}

集合

vector

我们要讲到的第一个类型是 Vec<T> ,也被称为 vectorvector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。

字符串

是字符的集合。Rust 核心语言只包含 &str , 字符串 String 由字符串标准库提供,而不编入核心语言。它是一种可增长、可变、可拥有 UTF-8 编码的字符串类型。

哈希 map

允许我们将值与一个特定的键相关联。哈希 map(hash map)。HashMap<K, V> 类型储存了一个键类型K 对应一个值类型 V 的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。

包管理

包:Cargo 的一个功能允许构建测试分享 crate。

包是提供一系列功能的一个或多个 crate。一个包会包含一个 cargo.toml 文件。阐述如何去构建这些 crate。包中可以包含至多一个库文件。任意多个二进制文件。但是必须至少包含一个 crate。

Crates

Crates:一个模块的树形结构,它形成了库或二进制项目。

crate 是 rust 编译时最小的代码单位。crate 可以包含模块,模块可以定义在其他文件中。crate 有两种形式:二进制项和库。二进制项可以被编译为可执行文件,必须包含 main 函数。库不包含 main 函数,也不会被编译为可执行文件。

crate root 是一个源文件,rust 编译器以它为起点。并构成你 crate 的跟模块。

rust 遵循一个约定就是 src/main.rs 就是一个与包同名的二进制 crate 的 crate根。src/lib.rs 表明包带有与库同名的 crate 库。且 src/lib.rs 是 crate 根。

模块和 use

模块和 use:允许你控制作用域和路径的私有性。

从 crate 的根节点开始,当编译一个 crate ,编译器首先在 crate 根文件寻找需要被编译的代码。

声明模块:在 crate 根文件中你可以声明一个新的模块。比如你用 mod garden,声明了一个叫做 garden 的模块,编译器会在下列路径寻找代码。

  • 内联:在大括号中,mod garden 后方不是一个分号而是大括号。
  • src/garden.rs
  • src/garden/mod.rs

声明子模块:比如你在 src/garden.rs 中用 mod vegetables 声明了一个叫做 vegetables 的模块。

  • 内联:在大括号中,mod vegetables 后方不是一个分号而是大括号。
  • src/garden/vegetables.rs
  • src/garden/vegetables/mod.rs

使用 use 关键词时一般只引用到父模块,然后通过父模块调用,用来区别本地函数。

可以使用 as 关键词提供新名称

#![allow(unused)]
fn main() {
use std::io::Result as IoResult;
use std::fmt::Result;
}

使用 pub use 重新导出名称

#![allow(unused)]
fn main() {
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();
}
}

使用 glob 运算符将所有公有定义引入作用域。经常用于测试模块

#![allow(unused)]
fn main() {
use std::collections::*;
}

模块树

#![allow(unused)]
fn main() {
mod front_of_house {
    mod hosting {
        fn add_to_wait_list() {}
        fn seat_at_table() {}
    }
    mod serving {
        fn take_order() {}

        fn server_order() {}

        fn take_payment() {}
    }
}
}

src/main.rs 和 src/lib.rs 被称为模块树, 之所以这样叫它们是因为这 两个文件的内容都分别在 crate 模块结构的根组成了一个名为 crate 的模块,该结构被称为 模块树(module tree)

crate
   front_of_house 
        hosting
            add_to_wait_list
            seat_at_table
        serving
            take_order
            server_order
            take_payment

模块中的代码路径

路径:一个命名例如结构体、函数或模块的方式。一旦一个模块是你 crate 的一部分,你可以在隐私规则的允许下,在同一 crate 的任意地方通过代码路径引用改模块的代码。vegetables 下的 Asparagus 类型可以被 crate::garden::vegetables::Asparagus 找到。

一个模块里的代码默认对父模块私有,为了使模块公有需要使用 pub mod 代替 mod。

use 关键字,在一个作用域内,use 关键字创建了一个成员的快捷方式,用来减少长路径的重复。

引用模块项目的路径

绝对路径

使用 crate 开头的绝对路径。

相对路径

使用 self、super或定义在当前模块中的标识符。

错误处理

Rust 将错误分为两大类:可恢复(recoverable)和不可恢复(unrecoverable)错误。对于一个可恢复的错误,比如文件未找到我们可能只想向用户报告并重试操作。不可恢复的错误总是bug 出现的征兆。比如试图访问一个超过数组末端的位置。

不可恢复错误:panic

panic! 处理不可恢复的错误,panic 时程序默认会展开,Rust 会回溯并清理遇到的每一个函数的数据。panic 通过在 Cargo.toml 中增加 panic = 'abort' 程序使用的内存由操作系统来清理。

[profile.release]
panic = 'abort'

有两种方法会造成 panic

  • 执行会造成 panic 的操作。比如执行数组越界访问。
  • 调用 panic!
fn main() {
    // 数组越界访问 panic
    let a = [1,2,3];
    println!("{}", a[4]);

    // 手动调用 panic!
    panic!("crash and burn");
}

可恢复错误:Result

大部分错误失败并没有严重到需要程序完全停止。有时候一个函数失败,仅仅就是因为一个容易理解和响应的原因。

#![allow(unused)]
fn main() {
enum Result <T, E> {
    Ok(T),
    Err(E),
}
}

Result 示例

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

fn main() {
    let f = File::open("hi.log");
    let mut file = match f {
        Ok(f) => {f}
        Err(e) => match e.kind() {
            ErrorKind::NotFound => match File::create("hi.log") {
                Ok(f) => f,
                Err(e) => panic!("Problem create the file {e:?}")
            } 
            other_error => {
                panic!("Problem opening the file: {other_error}");
            }
        },
    };

    let mut content = String::new();
    let _ = file.read_to_string(&mut content);
}

Result<T, E> 定义了很多方法来处理 match 复杂的问题。其中有一个方法教 unwrap ,如果 Result 的值是 Ok 那么 unwrap 会返回 Ok 中的值。如果 Result 的成员是 Errunwrap 会为我们调用 panic!expectunwrap 相同只不过可以定义错误消息。

传播错误

当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑 来决定应该如何处理错误。

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

fn main() {
    match read_username_from_file() {
        Ok(username) => {
            println!("{}", username)
        },
        Err(e) => {
            println!("{e:?}");
        }
    }
}

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut user_name_file = match username_file_result {
        Ok(f) => f,
        Err(e) => {
            return Err(e);
        }
    };

    let mut username = String::new();
    match user_name_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => {
            return Err(e);
        }
    }
}

传播错误的简写 ? 运算符

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

fn main() {
    match read_username_from_file() {
        Ok(username) => {
            println!("{}", username)
        },
        Err(e) => {
            println!("{e:?}");
        }
    }
}

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    let mut username_file = File::open("hello.txt")?;
    username_file.read_to_string(&mut username)?;

    // 链式调用进一步简化代码
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

Option<T> 上也可以使用 ? 运算符。

main 函数的返回值

main 函数的返回值也接受 Result 类型的枚举。如果返回值为 Ok 程序将正常退出。如果返回 Err 将以非 0 值退出。 main 函数也可以返回 sys::process::Termination Trait 的类型。

use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    Ok(())
}

泛型

我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的 具体数据类型。

在函数中定义泛型

当使用泛型函数时,本来在函数签名中指定参数和返回值类型的地方,会使用泛型来表示。

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for i in list {
        if i > largest {
            largest = i;
        }
    }
    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];
    for i in list {
        if i > largest {
            largest = i;
        }
    }
    largest
}

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for i in list {
        if i > largest {
            largest = i;
        }
    }
    largest
}

fn main() {
    let number_list = vec![10, 20, 30, 40, 50];
    println!("The largest number is {}", largest_i32(&number_list));
    println!("The largest number is {}", largest(&number_list));

    let char_list = vec!['y', 'm', 'a'];
    println!("The largest char is {}", largest_char(&char_list));
    println!("The largest char is {}", largest(&char_list));
}

在结构体定义泛型

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}
fn main() {
    let p = Point{x:1, y:2};
    println!("{:?} x={}, y={}", p, p.x, p.y);
    let p = Point{x:1.1, y:2.2};
    println!("{:?} x={}, y={}", p, p.x, p.y);
}

在枚举中定义泛型

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

方法中定义泛型

在为结构体和枚举实现方法时也一样可以使用泛型。必须在 impl 后面声明 Trust 就知道 Point<T> 尖括号中的 T 是泛型而不是具体的类型。 我们可以为泛型参数选择一个与结构体泛型参数不同的名字。不过依照惯例使用了相同的名称。

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

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }

    fn y(&self) -> &T {
        &self.y
    }
}
fn main() {
    let p = Point{x: 1, y: 2};

    println!("{}", p.x());
    println!("{}", p.y());
}

与结构体参数名不同

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

impl<U> Point<U> {
    fn x(&self) -> &U {
        &self.x
    }

    fn y(&self) -> &U {
        &self.y
    }
}

fn main() {
    let p = Point{x: 1, y: 2};

    println!("{}", p.x());
    println!("{}", p.y());
}

定义方法时也可以为泛型指定限制

#![allow(unused)]
fn main() {
impl<f32> Point<f32> {
    fn x(&self) -> &f32 {
        &self.x
    }

    fn y(&self) -> &f32 {
        &self.y
    }
}
}

方法的泛型与结构体或枚举不通

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, p: Point<X2, Y2>) -> Point<X1, Y2> {
        Point { x: self.x, y: p.y }
    }
}
fn main() {
    let p = Point { x: 1, y: 1.1 };
    let p2 = Point { x: "hello", y: 'y' };
    let p3 = p.mixup(p2);
    println!("{} {}", p3.x, p3.y)
}

泛型代码的性能

泛型代码不会增加运行时开销,通过在编译时将泛型单态化来保证效率。通过填充编译时使用的具体类型,将通用代码转化为特定代码的过程。

Trait

定义共同行为,trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共同行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

定义 trait

一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类 型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实 现某些目的所必需的行为的集合。

使用 trait 来声明一个 trait,后面是 trait 的名字。大括号中是方法的签名。在方法签名后跟分号而不是大括号及其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 Summary trait 的类型都拥有与这个 签名的定义完全一致的 summarize 方法。

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
}

实现 trait

当 trait 或者 类型至少有一个属于当前 crate 才能对类型实现该 crate。不能为外部类型实现外部 trait,这个被称为相干性。孤儿规则。

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{} by {},{}", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
}

调用 trait

trait 必须和类型一起引入作用域以便使用额外的 trait 方法。

use crate::aggregator::{NewsArticle, Summary, Tweet};

mod aggregator;

fn main() {
    let news_article = NewsArticle{
        headline: "Look".to_string(),
        location: "Shan Xi".to_string(),
        author: "Han ZhuoXian".to_string(),
        content: "Look at the man".to_string(),
    };

    println!("news article: {}", news_article.summarize());

    let tweet = Tweet {
        username: "zhuoxian".to_string(),
        content: "I like you!".to_string(),
        reply:false,
        retweet:false,
    };

    println!("tweet: {}", tweet.summarize());
}

默认实现

为方法提供默认行为,为某个类型实现时可以选择覆盖默认行为。

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String {
        "Read from ...".to_string()
    }
}
}

Trait 作为参数

#![allow(unused)]
fn main() {
// Trait Bound 语法 适合更复杂的场景
pub fn notify_bound<T: Summary> (item : &T) {
    println!("News : {}", item.summarize());
}

// &impl 是 Trait Bound 语法的语法糖,适合简短的例子
pub fn notify(item: &impl Summary) {
    println!("News : {}", item.summarize());
}

// 使用 + 指定多个 trait bound
pub fn notify_bound_display<T: Summary + Display>(item: &T) {
    println!("News : {}", item.summarize());
}

// 多个 trait bound 的语法糖
pub fn notify_display(item: &(impl Summary + Display)) {
    println!("News : {}", item.summarize());
}

// 使用 where 子句简化函数签名
pub fn some<T, U>(t: &T, u: &U)
where
    T: Display + Clone,
    U: Clone + Debug,
{
    println!("some {} {:?}", t, u);
}

}

使用 trait bound 有条件的实现方法

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。

use std::fmt::Display;

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

struct Own {

}
// 为所有 Pair 结构体实现 new 方法
impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x: x, y: y }
    }
}

// 限定只有实现 T 实现了 Display 和 PartialOrd 才能使用该方法。 
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("x >= y");
        } else {
            println!("x < y");
        }
    }
}

fn main() {
    let pair = Pair::new(1, 2);
    pair.cmp_display();

    let o_x = Own{};
    let o_y = Own{};
    let pair = Pair::new(o_x, o_y);
    // pair.cmp_display();// 不能调用该方法
    
}

对任何实现了特定 trait 的类型实现方法 blanket implementations。

#![allow(unused)]
fn main() {
impl <T: Display> ToString for T {

}
}

生命周期

Rust 中的每一个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。大多数生命周期是隐含并且可以推断的。对于一些无法推断的场景需要使用泛型生命周期参数来注明他们的关系。

生命周期避免了悬垂引用,引用了非预期引用的数据。变量已经释放但是还持有该变量地址的引用,里边的数据可能已经被其他变量使用。

借用检查器 (borrow checker) 检查 r 的生命周期 ’a 引用了 生命周期 ’b 的对象 x,所以拒绝编译。因为 ’b 的生命周期 比 ’a 的生命周期小的多。

fn main() {
    let r;                // -------------+- 'a
    {
        let x = 5;       // -----+- 'b    |
        r = &x;          // -----+| 'b    |
    }
    println!("{r}");     // -------------+- 'a
}
fn main() {
    let r;               // -------------+- 'a
    {
        let x = 5;       // -----+- 'b    |
        r = &x;          //      |        |
        println!("{r}"); // ------------- | 'a
                         // -----| 'b     |
    }
    println!("{r}");     // --------------| 'a  检查无效
}

函数中标注生命周期参数

生命周期注解并不改变任何引用的生命周期的长短。相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。

生命周期参数名称必须以撇号(’ )开头,其名称通常全是小写,类似于泛型其名称非常短。大多数人使用 ’a 作为第一个生命周期注解。生命周期参数注解位于引用的 & 之后,并有一个空格来将引用类型与生命周期注解分隔开。

helle 变量的引用:&hello helle 生命周期为 ’a 变量的引用 :&'a hello helle 生命周期为 ’a 变量的可变引用 &'a mut hello

#![allow(unused)]
fn main() {
// 表明返回值与两个参数存活的一样久,当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}
}

当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果 返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然 而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 longest 函数实现:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    let result = String::from("hello");
    &result // 不能返回函数内变量
}
}

结构体定义中的生命周期注解

结构体也可以定义包含引用的字段,不过需要为每一个引用添加生命周期注解。


struct ImportantExcerpt<'a> {
    part: &'a str,
}
fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt{
        part: first_sentence,
    };

    println!("{}", i.part);
}

生命周期省略规则

函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。

规则一:编译器为每个生命周期分配一个生命周期参数。函数有一个引用就有一个生命周期参数 fn foo<'a>(x: &'a str)fn foo<'a>(x: &'a str, y: &'b str)。 规则二:如果只有一个输入生命周期参数那么它被赋予所有输出生命周期参数。fn foo<'a>(x: &'a str) -> (first: &'a str, last: &'a str)。 规则三:如果有多个生命周期参数并且其中有一个是 &self&mut self 说明是个对象的方法。那么所有输出生命周期参数被赋予 self 的生命周期 fn part(&'a self, x: &'b str) -> &'a str

静态生命周期

'static 生命周期存活于整个程序期间。所有字符串字面值都拥有 'static 生命周期。let s: &'static str = "hello"; 可以标注出来。

测试

测试函数是用来验证非测试代码是否能按照预期的方式进行。

测试函数的流程

  • 设置任何所需的数据或者状态。
  • 运行需要测试的代码
  • 断言结果

Rust 中测试函数就是带有 test 属性注解的函数。

最简单的测试函数

#![allow(unused)]

fn main() {
#[test]
fn assert() {
    assert!(true);
}
}

测试模块组织的测试函数

#![allow(unused)]
fn main() {
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}
}

自定义错误消息

assert! 第二个之后的参数都会传给 format!

#![allow(unused)]
fn main() {
    #[test]
    fn assert_msg() {
        assert!(false, "I am false {}", "ff");
    }
}

断言 panic

expected 参数可以指定 panic 的错误信息。

#![allow(unused)]
fn main() {
    #[test]
    #[should_panic(expected="div_zero")]
    fn div_zero() {
        panic!("div_zero");
    }
}

返回 Result<T, E> 用于测试

在测试通过时返回 Ok 失败时返回 Err

#![allow(unused)]
fn main() {
    #[test]
    fn result() -> Result<(), String> {
        let i = 4; 
        if i == 3 {
            Ok(())
        } else {
            Err(String::from("error"))
        }
    }
}

控制测试运行

cargo test 在测试模式下编译代码并运行生成的测试二进制文件。cargo test 默认并发运行所有的测试。 可以将一部分参数传递给 cargo test ,另一部分传递给测试二进制程序。使用 – 进行分割。

显示函数输出

在测试中调用了 println! 而测试通过了我们不会看到任何输出,如果测试失败了将看到所有标准输出和其他错误信息。

指定名称运行部分测试

# 运行单个测试 名称支持全匹配和部分匹配
cargo test assert_msg

忽略测试

#![allow(unused)]

fn main() {
#[test]
#[ignore]
fn assert() {
    assert!(true);
}
}

只运行忽略的测试

# 运行全部测试不包含 ignored 的测试
cargo test
# 只运行 ignored 的测试
cargo test -- --ignored
# 运行所有测试
cargo test -- --include-ignored

单元测试

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确地验 证某个单元的代码功能是否符合预期。单元测试与它们要测试的代码共同存放在位于 src 目录 下相同的文件中。规范是在每个文件中创建包含测试函数的 tests 模块,并使用 cfg(test) 标注模块。

#[cfg(test)] 注解告诉 Rust 只在执行 cargo test 时才编译和运行测试代码。而在运行 cargo build 时不需要

集成测试

集成测试需要新建 tests 目录与 src 目录同级。可以在这个目录中新建测试文件,会将每个文件当作一个 crate 来编译。


cargo test --test 集成测试文件名

闭包与迭代器

闭包

闭包是可以保存在变量中或作为参数传递给其他函数的匿名函数。闭包会捕获其环境。

闭包类型推断和注解

闭包通常不要求像函数一样对参数与返回值进行类型注解。闭包通常较短并与特定上下文相关,而不适用于任意情景。

#![allow(unused)]
fn main() {
    // 完整函数展示
    fn add_one_v1(x: u32) -> u32 {
        x + 1
    }
    // 完整标注闭包定义
    let add_one_v2 = |x: u32| -> u32 { x + 1 };
    // 省略类型注解
    let add_one_v3 = |x| {x + 1};
    // 省略大括号
    let add_one_v4 = |x| x + 1;
    // 调用 v3 v4 是代码能编译的必要条件,否则无法推断类型。
    add_one_v3(1);
    add_one_v4(1);
}

捕获引用或移动所有权

闭包可以通过三种方式捕获其环境中的值,它们直接对应到函数获取参数的三种方式:不可变借用、可变借用和获取所有权。闭包将根据函数体中对捕获值的操作来决定使用哪种方式。

将被捕获的值移出闭包和 Fn trait

一旦闭包捕获了定义它的环境中的某个值的引用或所有权(也就影响了什么会被移 进 闭包, 如有),闭包体中的代码则决定了在稍后执行闭包时,这些引用或值将如何处理(也就影响了 什么会被移 出 闭包,如有)。闭包体可以执行以下任一操作:将一个捕获的值移出闭包,修改 捕获的值,既不移动也不修改值,或者一开始就不从环境中捕获任何值。

闭包捕获和处理环境中的值的方式会影响闭包实现哪些 trait,而 trait 是函数和结构体指定它 们可以使用哪些类型闭包的方式。根据闭包体如何处理这些值,闭包会自动、渐进地实现一 个、两个或全部三个 Fn trait。

  1. FnOnce 适用于只能被调用一次的闭包。所有闭包至少都实现了这个 trait,因为所有闭包都 能被调用。一个会将捕获的值从闭包体中移出的闭包只会实现 FnOnce trait,而不会实现其 他 Fn 相关的 trait,因为它只能被调用一次。
  2. FnMut 适用于不会将捕获的值移出闭包体,但可能会修改捕获值的闭包。这类闭包可以被 调用多次。
  3. Fn 适用于既不将捕获的值移出闭包体,也不修改捕获值的闭包,同时也包括不从环境中捕 获任何值的闭包。这类闭包可以被多次调用而不会改变其环境,这在会多次并发调用闭包 的场景中十分重要。

迭代器

迭代器模式允许你依次对一个序列中的项执行某些操作。迭代器(iterator)负责遍历序列中 的每一项并确定序列何时结束的逻辑。使用迭代器时,你无需自己重新实现这些逻辑。Rust 中迭代器是 惰性的。

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item; // 关联类型
    fn next(&mut self) -> Option<Self::Item>; // 获取下一个元素
}

}

消费迭代器的方法

一些方法在定义中调用了 next 方法,这些调用 next 的方法被称为消费者适配器。

迭代器适配器

他们不会消耗迭代器而是通过改变原始迭代器的某些方面生成不通的迭代器。

智能指针

指针是一个包含内存地址的变量的通用概念。

智能指针是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。

Box<T>

最简单直接的智能指针是 box,其类型是 Box<T> 。box 允许你将一个值放在堆上而不是栈上,留在栈上的是指向堆数据的指针。

应用场景:

  1. 当有一个在编译时大小未知的类型。而又想要在需要确切大小的上下文中使用这个类型值的时候。
  2. 当有大量数据并希望确保数据不被拷贝的情况下转移所有权的时候。
  3. 当希望拥有一个值并只关心他的类型是否实现了特定 trait 而不是具体类型的时候。
fn main() {
    let b = 5;
    println!("b = {b}");

    let c = Box::new(3);
    println!("box c = {c}");
}

我们可以像数据是存储在栈上的那样访问数据。正如任何拥有数据所有权的值那样。当 c 离开作用域的时候,它将被释放,这个释放作用域 box 本身(栈内存)和指向的数据(堆内存)。

Box 允许创建递归类型

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};
fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

    println!("{:?}", list);
}

Deref trait

实现 Deref trait 允许我们重载解引用运算符(dereference operator)*。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待。

fn main() {
    let x = 5;
    let y = &x; // y 等于 x 的一个引用

    assert_eq!(5, x);
    assert_eq!(5, *y); // 使用 *y 来访问引用指向的值。
}

Rust 使用 DerefMut trait 用于重载可变引用的 * 运算符。

Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换。

  • T: Deref<Target=U> 时从 &T&U
  • T: Deref<Target=U> 时从 &mut T&mut U
  • T: Deref<Target=U> 时从 &mut T&U

类型强制转化的一个例子

use std::{fmt::Display, ops::{Deref, DerefMut}};

#[derive(Debug)]
struct One<T: Display> {
    inner: OneOne<T>,
}

#[derive(Debug)]
struct OneOne<T: Display> {
    v: T,
}

impl<T: Display> Deref for One<T> {
    type Target = OneOne<T>;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl <T:Display> DerefMut for One<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.inner
    }
}

fn main() {
    let one = One {
        inner: OneOne { v: 1 },
    };
    print_one_one(&one);

    let mut mut_one = One{
        inner: OneOne { v: 2 },
    };
    print_one_one(&mut mut_one);

    update_one_one(&mut mut_one, 3);

    print_one_one(&mut mut_one);

}

fn print_one_one<T: Display>(one_one: &OneOne<T>) {
    println!("{}", one_one.v);
}


fn update_one_one<T:Display>(one_one: &mut OneOne<T>, u: T) {
    one_one.v = u
}

rop trait 运行清理代码

对于智能指针模式来说,第二个重要的 trait 是 Drop 允许我们在变量离开作用域时执行一些代码。

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Drop CustomSmartPointer data with: `{}`!", self.data);
    }
}
fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };

    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointer created");
}

Rc<T>

use crate::List::{Cons, Nil};
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count: {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a)); // b 会克隆 a 包含的 Rc<List>, 引用计数由 1 变为 2
    println!("count: {}", Rc::strong_count(&a));

    {
        let c = Cons(4, Rc::clone(&a)); // c 会克隆 a 包含的 Rc<List>, 引用计数由 2 变为 3
        println!("c : {c:?}");
        println!("inner count: {}", Rc::strong_count(&a));
    } // c 超出作用域, 引用计数由 3 变成 2
    println!("count: {}", Rc::strong_count(&a));
    println!("b : {b:?}");
}

RefCell<T> 内部可变性模式

允许在即使有不可变引用时也可以改变数据。这通常是借用规则不允许的。为了改变数据,该数据结构使用 unsafe 来模糊 Rust 可变性和借用规则。使用 unsafe 表明我们在手动检查这些规则而不是让编译器检查。

不同于 Rc<T> RefCell<T> 代表数据的唯一所有权。对于 Rc Box 不可变性作用域编译时,而对于RefCell<T> 不可变性作用于运行时。对于引用如果运行时违反这些规则,程序会 panic并退出。

borrow_mut 返回了 RefMut 类型的智能指针, borrow 返回了 Ref 类型的智能指针。这两个类型都实现了 Deref,可以当作常规引用对待。

RefCell<T> 记录当前有多少个活动的 Ref<T>RefMut<T> 智能指针。每次调用 borrowRefCell<T> 将活动的不可变借用计数加一。当 Ref<T> 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。

并发

并发:代表程序的不同部分互相独立的运行。 并行:代表程序的不同部分同时运行。

线程

use std::{thread, time::Duration};

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();
    
    for i in 1..5 {
        println!("hi number {i} from the mail thread!");
        thread::sleep(Duration::from_millis(1));
    }

}

多生产者 channel

use std::{sync::mpsc, thread, time::Duration};

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

    thread::spawn(move ||{
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thead"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });


      thread::spawn(move ||{
        let vals = vec![
            String::from("more"),
            String::from("message"),
            String::from("for"),
            String::from("you"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

共享状态并发

互斥器

互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。 为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希 望访问数据。 锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护(guarding)其数据。

Arc

原子引用计数,线程安全的。线程安全带有性能惩罚。所以在单线程环境中 Rc 是符合需求且最高效的。

面向对象

面向对象的程序由对象组成。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法 或 操作。

面向对象的三个特性 封装:pub 关键词暴露外部公共接口 继承:一个对象可以从另一个对象继承元素而无需再次定义。 多态

模式匹配

模式

模式是 Rust 中的特殊语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。

模式由以下内容组成:

  • 字面值
  • 已解构的数组、枚举、结构体或元组
  • 变量
  • 通配符
  • 占位符

所有可能会用到模式的位置

match 分支

在形式上 match 表达式由 match 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式,如下所示:

#![allow(unused)]
fn main() {
match {
    pattern => expression,
    pattern => expression,
    pattern => expression,
}
}

NoneSome(i)

#![allow(unused)]
fn main() {
match x {
    None => None,
    Some(i) => Some(i + 1),
}
}

if let 条件表达式

只关心一个情况的 match 表达式的简写。可以组合并匹配 if letelse ifelse if let 并不要求他们互相关联。

while let 条件循环

只要模式匹配就一直执行 while 循环。

for 循环

let 语句

#![allow(unused)]
fn main() {
let PATTERN = EXPRESSION;
}

变量名位于 PATTERN 所的位置,变量名是形式特别朴素的模式。我们将表达式与模式比较,并为任何找到的名称赋值。let x = 5 中 , x 代表的是 “将匹配到的值绑定到变量 x” 的模式。同时因为 x 是整个模式,这个模式实际上等于 “将任何值绑定到变量x,不管值是什么”。

函数参数

函数参数也可以是模式。

可反驳性

Refutability (可反驳性)模式是否会匹配失效。

模式有两种模式:refutable (可反驳的)和 irrefutable (不可反驳的)。能匹配任何传递的可能值的模式被称为是“不可反驳的(irrefutable)”。 对某些可能的值进行匹配会失败的模式被称为是 “可反驳的(refutable)”。

函数参数 、 let、 for 只能接受不可反驳模式,if let 、while let 可以接受可反驳和不可反驳的。

模式语法

匹配字面值

fn main() {
    let x = 7;
    match x {
        1 => println!("one"),               // 普通字面值
        2 | 3 => println!("two or three"),  // 多个模式
        4..=5 => println!("4-5"),           // 范围范围-右闭区间
        6..7 => println!("6"),              // 匹配范围-右开区间
        other => println!("other {other}"), // 匹配剩余并绑定变量
        _ => println!("other"),             // 匹配剩余不绑定变量
    }
}

匹配命名变量

fn main() {
    let x = Some(5);
    let y = 10;
    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("y = {y}"),
        _ => println!("default case x={:?}", x),
    }

    println!("main y={}", y);
}

解构结构体

struct Point {
    x: u32,
    y: u32,
}
fn main() {
    let point = Point { x: 1, y: 2 };
    let Point { x: a, y: b } = point;
    println!("a = {} b = {}", a, b);
}

解构枚举

use std::arch::x86_64;

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
fn main() {
    let message = Message::ChangeColor(0, 160, 255);
    match message {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move: {x}{y}"),
        Message::Write(s) => println!("Write: {s}"),
        Message::ChangeColor(r, g, b) => println!("ChangeColor {r} {g} {b}")
    }
}

解构结构体和枚举嵌套

匹配守卫

fn main() {
    // let num = Some(6);
    let num = None::<i32>;
    match num {
        Some(x) if x < 5 => println!("less than five: {x}"),
        Some(x) => println!("{x}"),
        None => (),
    }
}

@绑定

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let message = Message::Hello{id: 12};
    match message {
        Message::Hello { 
            id: id_var @3..7,
         } => {
            println!("id_var: {}", id_var);
        },
        Message::Hello { id: 10..=12 } => {
            println!("10~12");
        }
        Message::Hello { id } => {
            println!("id {}", id)
        }
    }

}

不安全的 Rust

rust 不执行强制安全保证。通过使用 unsafe 来切换到不安全的 Rust。这里有五类能在不安全的 Rust 而不能在安全的 Rust 操作。

  1. 解引用裸指针。
  2. 调用不安全的函数或方法。
  3. 访问或修改可变静态变量。
  4. 实现不安全 trait。
  5. 访问 union 字段。

unsafe 并不会关闭借用检查器或禁用其他任何安全检查。

解引用裸指针

不可变裸指针 *const T 可变裸指针 *mut T* 不是解引用运算符而是类型名称的一部分。可以在不安全的代码外创建裸指针,知识不能在不安全的代码解

裸指针特性:

  1. 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针。
  2. 不保证指向有效的内存。
  3. 允许为空。
  4. 不能实现任何自动清理。
fn main() {
    let mut num = 5;
    let r1 = &num as *const i32; // 将不可变引用转换为裸指针
    let r2 = &mut num as *mut i32; // 将可变应用转化为裸指针

    let address = 0x012345usize;
    let _r = address as *const i32; // 创建指向任意类型的裸指针

    // 只能在不安全块中解引用裸指针
    unsafe {
        println!("r1 {}", *r1);
        println!("r2 {}", *r2);
    }
}

调用不安全函数或方法

fn main() {
    unsafe fn dangerous() {
        let num = 5;
        let r = &num as *const i32;
        unsafe {
            println!("dangerous is {}", *r);
        }
    }
    unsafe {
        dangerous();
    }
}

使用 extern 函数调用外部代码

extern 有助于创建和使用外部函数接口(Foreign Function Interface, FFI)。外部函数接口是一个编程语言用一定义函数的方式,其允许不同外部编程语言调用这些函数。

unsafe extern "C" {
   unsafe fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -5 is {}", abs(-5));
    }
}

访问或修改可变静态变量

静态变量只能存储拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显示标注。访问不可变静态变量是安全的。

static mut COUNTER: i32 = 0;
fn add_to_count(inc: i32) {
    unsafe {
        COUNTER += inc;
    }
}


fn main() {
    add_to_count(3);
    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

实现不安全 trait

trait 中至少有一个方法中包含编译器无法验证的不变式时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe。 同时 trait 的实现也必须标记为 unsafe

访问联合体中的字段

仅适用于 unsafe 的最后一个操作是访问 联合体中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。

高级 trait

关联类型在 trait 定义中指定占位符类型

关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。 trait 的实现者会针对特定的实现在这个占位符类型置顶相应的具体类型。如此可以定义一个可以使用多种类型的 trait,知道实现此 trait 时 都无需知道这些类型具体时什么。

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
}

Item 是一个占位符类型,同时 next 方法定义表明它返回 OptionSelf::Item 类型的值。这个 trait 的实现者会指定 Item 的具体类型。 然而不管实现者置顶何种类型,next 方法都会返回一个包含了此具体类型值的 Option。

关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。让我们通过在一个 Counter 结构体上实现 Iterator trait 的例子来检视其中的区别。这哥是现在宏指定了 Item 的类型为 u32:

默认泛型类型参数和运算符重载

<Rhs = Self> 这个语法叫做默认类型参数(default type parameters)。Rhs 是一个泛型类型参数(“right hand side”)的缩写。 它用于定义 add 方法中的 rhs 参数。如果实现 Add trait 时不指定 Rhs 的具体类型, Rhs 的类型将时默认的 Self 类型。也就是在骑上实现的 Add 类型。

扩展类型而不破坏现有代码。 在大部分用户都不需要的特定情况下进行自定义。

#![allow(unused)]
fn main() {
pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
}

完全限定语法与消歧义:调用相同名称的方法

完全限定语法

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

newtype 模式 用以在外部类型上实现外部trait

类型别名用来创建类型同义词

类型别名不是一新的单独的类型,完全被当作相同的类型对待。

fn main() {
    type Kilometers = i32;
    let x: i32 = 5;
    let y: Kilometers = 5;
    println!("x + y = {}", x + y);
}

动态大小类型和 Size trait

有时被称为 DSTunsized types ,这些类型允许我们处理只有在运行时才知道大小的类型。 为了处理 DST ,Rust 提供了 Sized trait 来决定一个类型的是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。 Rust

高级类型

高级函数与闭包

函数指针

函数满足类型 fn,fn 被称为函数指针。通过函数指针允许我们使用函数作为另一个函数的参数。

函数指针实了所有三个闭包 trait (Fn,FnMut,FnOnce),所以总是可以在调用闭包的函数时传递函数指针作为参数。 倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。

返回闭包

闭包表现为 trait ,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。

宏是一种为写其他代码而写代码的方式,即所谓的元编程。宏在编译器翻译代码前展开。

声明宏

使用 marco_rules! 的声明宏用于通用元编程。

#![allow(unused)]
fn main() {
#[macro_export] // 表明只要导入了该crate就可以使用该宏
macro_rules! my_vec { // macro_rules! 定义宏 vec 宏名称
    ($($x:expr),*) => { // 与 match 类似,$x:expr 表示匹配一个表达式,$()表示重复,*表示重复0次或多次
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
}

过程宏

他们接受一个代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出。而非像声明宏那样匹配对应模式然后以另一部分代码替换当前代码。

有三种类型的过程宏: 自定义派生(derive)宏

#![allow(unused)]

fn main() {
}

类属性宏

可以创建新的属性。

#![allow(unused)]
fn main() {
#[route(GET, '/index')]
fn index() {}

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {}
}

类函数(Function-Like)宏 看起来像函数调用的宏

#![allow(unused)]
fn main() {
let sql = sql!(SELECT * FROM posts WHERE id=1);

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {}
}

serde

Serde 是一个用于对 Rust 数据进行序列化和反序列化的框架 高效、通用地构建。

clap

Derive 教程

快速开始

添加 clap derive 依赖

cargo add clap --features derive

预览一个应用程序

use std::path::PathBuf;

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    /// 要操作的名字
    name: Option<String>,

    /// 设置自定义配置文件
    #[arg(short, long, value_name = "FILE")]
    config: Option<PathBuf>,

    /// 开启调试
    #[arg(short, long, action = clap::ArgAction::Count)]
    debug: u8,

    /// 子命令
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// 测试
    Test {
        /// 列出测试的值
        #[arg(short, long)]
        list: bool,
    },
}

fn main() {
    let cli = Cli::parse();

    // You can check the value provided by positional arguments, or option arguments
    if let Some(name) = cli.name.as_deref() {
        println!("Value for name: {name}");
    }

    if let Some(config_path) = cli.config.as_deref() {
        println!("Value for config: {}", config_path.display());
    }

    // You can see how many times a particular flag or argument occurred
    // Note, only flags can have multiple occurrences
    match cli.debug {
        0 => println!("Debug mode is off"),
        1 => println!("Debug mode is kind of on"),
        2 => println!("Debug mode is on"),
        _ => println!("Don't be crazy"),
    }

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    match &cli.command {
        Some(Commands::Test { list }) => {
            if *list {
                println!("Printing testing lists...");
            } else {
                println!("Not printing testing lists...");
            }
        }
        None => {}
    }

}

配置 Parser

代码指定

use clap::Parser;

#[derive(Parser)]
#[command(name = "MyApp")]
#[command(version = "1.0")]
#[command(about = "Does awesome things", long_about = None)]
struct Cli {
    name: Vec<String>,
    #[arg(long)]
    two: String,
    #[arg(long)]
    one: String,
}

fn main() {
    let cli = Cli::parse();
    println!("name: {:?}", cli.name);
    println!("two: {:?}", cli.two);
    println!("one: {:?}", cli.one);
}

运行程序

$ cargo run -- --help
# 查看帮助信息
Does awesome things

Usage: clap_config --two <TWO> --one <ONE>

Options:
      --two <TWO>
      --one <ONE>
  -h, --help       Print help
  -V, --version    Print version

$ cargo run -- --version
# 查看版本
MyApp 1.0

$ cargo run -- --two=2 --one=1
# 程序输出获取的参数
two: "2"
one: "1"

从 cargo.toml 读取

use clap::Parser;

#[derive(Parser)]
#[command(name = "MyApp")]
#[command(version, about)]
struct Cli {
    #[arg(long)]
    two: String,
    #[arg(long)]
    one: String,
}

fn main() {
    let cli = Cli::parse();
    println!("two: {:?}", cli.two);
    println!("one: {:?}", cli.one);
}

cargo.toml

[package]
name = "clap_config_from_cargo"
version = "0.1.0"
edition = "2024"
authors = ["hanjian <hanjian@zhouyi.com>"]
description = "A simple CLI app using clap"

[dependencies]
clap = { version = "4.5.53", features = ["derive"] }

运行命令查看

$ cargo run -- --help

A simple CLI app using clap

Usage: clap_config_from_cargo --two <TWO> --one <ONE>

Options:
      --two <TWO>
      --one <ONE>
  -h, --help       Print help
  -V, --version    Print version


cargo run -- -V
MyApp 0.1.0

next_line_help

use clap::Parser;

#[derive(Parser)]
#[command(name = "MyApp")]
#[command(version, about)]
#[command(next_line_help = true)]
struct Cli {
    #[arg(long)]
    two: String,
    #[arg(long)]
    one: String,
}

fn main() {
    let cli = Cli::parse();
    println!("two: {:?}", cli.two);
    println!("one: {:?}", cli.one);
}

执行程序查看结果,帮助信息会在下一行展示

$ cargo run -- --help
A simple CLI app using clap

Usage: clap_config_from_cargo --two <TWO> --one <ONE>

Options:
      --two <TWO>

      --one <ONE>

  -h, --help
          Print help
  -V, --version
          Print version

添加参数

位置参数

默认结构体字段会被定义为位置参数

use clap::Parser;

#[derive(Parser)]
#[command(name = "MyApp")]
#[command(version, about)]
#[command(next_line_help = true)]
struct Cli {
    name: String,
}

fn main() {
    let cli = Cli::parse();
    println!("name:{:?}", cli.name);
}

执行命令查看输出

$ cargo run -- hzx
name:"hzx"

接收多个值

use clap::Parser;

#[derive(Parser)]
#[command(name = "MyApp")]
#[command(version, about)]
struct Cli {
    name: Vec<String>,
}

fn main() {
    let cli = Cli::parse();
    println!("name:{:?}", cli.name);
}

执行命令并查看,可以接收多个值

$ cargo run -- hzx hj
name:["hzx", "hj"]

选项

可以使用 #[arg(short='n')]#[arg(long=name)] 属性在一个字段上。当没有值提供时如 #[arg(short,long)] 将会使用字段名称。

use clap::Parser;

struct Cli {
    #[arg(short, long)]
    name: String,
}
fn main() {
    let cli = Cli.parse();

    println!("name: {:?}", cli.name);
}

执行程序

$ cargo run -- --name bob
$ cargo run -- --name=bob
$ cargo run -- -n bob
$ cargo run -- -n=bob
$ cargo run -- -nbob

name: "bob"

接收多个值

use clap::Parser;

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    name: Vec<String>,
}

fn main() {
    let cli = Cli::parse();

    println!("name: {:?}", cli.name);
}
$ cargo run -- --name bob --name jam
name: ["bob", "jam"]

标志(Flags)

Flags 是一个开关,如果指定该参数就是开,不指定就是关

use clap::Parser;

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();

    println!("verbose: {:?}", cli.verbose);
}

执行程序并查看结果

$ cargo run -- -v
verbose: true
$ cargo run --
verbose: false

可选参数

use clap::Parser;

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    name: Option<String>,
}

fn main() {
    let cli = Cli::parse();

    println!("name: {:?}", cli.name);
}
$ cargo run
name: None
$ cargo run jam
name: Some("jam")

默认值

use clap::Parser;

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[arg(default_value_t = 2020)]
    port: u16,

    #[arg(default_value = "127.0.0.1")]
    host: String,
}

fn main() {
    let cli = Cli::parse();

    println!("host: {:?}", cli.host);
    println!("port: {:?}", cli.port);
}
$ cargo run
host: "127.0.0.1"
port: 2020
$ cargo run 22
host: "127.0.0.1"
port: 22

参考链接