Rust 基础

参考 Rust DocumentationTour of Rustcrates.io

Getting Started

Hello, World!

1
2
3
fn main() {
println!("Hello, world!");
}
1
2
3
$ rustc main.rs
$ ./main
Hello, world!

Common Programming Concepts

Variables and Mutability

使用 let 声明变量,变量默认是不可变的,使用 mut 关键字使其可变。使用 const 声明常量,不允许对常量使用 mut,并且必须显示指定其类型。

1
2
let mut x = 5;
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

和其他语言不同,Rust 允许在相同作用域中遮蔽(shadowing)变量,而且允许使用不同的类型。

1
2
let spaces = "   ";
let spaces = spaces.len();

Data Types

Rust 数据类型分为两种:标量类型(scalar)和复合类型(compound)。有四种基本的标量类型:整型、浮点类型、布尔类型和字符类型。编译器通常可以推断出类型,如果不能则需要显式写出类型。

1
let guess: u32 = "42".parse().expect("Not a number!");

Scalar Types

有符号整型有 i8i16i32i64i128isize,其中 isize 类型和机器相关。无符号整型类似,只是将 i 开头替换为 u 开头。整型字面量:十进制 98_222、十六进制 0xff、八进制 0o77、二进制 0b1111_0000 和单字节字符 b'A'(仅限 u8 类型)。可以使用标准库函数显示处理整型溢出:wrapping_*checked_*overflowing_*saturating_*

浮点类型有 f32f64,默认使用 f64。布尔类型 bool 的值有 truefalse 两种。字符类型 char 大小为 4 字节,使用 UTF-32 编码方式。

Compound Types

Rust 有两个原生的复合类型,元组(tuple)和数组(array)。元组可以将一个或多个不同类型的值组合起来,声明之后长度固定不变。可以使用 .index、模式匹配(pattern matching)和解构(destructure)获取元组元素,其中 index 是元素的索引。不包含任何值的元组 () 被称为单元(unit),表示空值或空返回类型,如果表达式不返回其他值,则会隐式地返回单元值。

1
2
3
4
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
1
2
3
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");

数组中每个元素的类型都必须相同,数组长度也是固定的,在栈上分配空间。在声明类型时,需要在方括号中包含元素类型和数量。在初始化时,可以使用方括号包含初始值和元素数量,创建每个元素都相同的数组。

1
2
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5];

Functions & Control Flow

Rust 代码中的变量和函数名使用 snake case 风格,使用下划线分隔单词。Rust 是基于表达式的语言(expression-based),函数体由一系列语句(statement)和可选地结尾表达式(expression)组成,语句不会返回值而表达式会。表达式结尾没有分号,如果加上分号它就变为语句。如果函数没有返回值,则可以省略返回类型,会隐式地返回空元组,否则需要使用 -> 显式声明。代码块 {}if 都是表达式,可以在 let 语句右侧使用。

1
2
3
fn five() -> i32 {
5
}
1
2
3
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");

Rust 有三种循环 loopwhileforloop 表示无限循环,可以使用 break 终止并指定返回值。

1
2
3
4
5
6
7
8
9
10
let mut counter = 0;

let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};

println!("The result is {result}");
1
2
3
4
5
for i in 0..n { ... }
for i in (0..n).rev() { ... }
for item in &arr { ... }
for item in &mut arr { ... }
for (i, item) in arr.iter().enumerate() { ... }

Understanding Ownership

What Is Ownership?

所有权(ownership)是 Rust 用于管理内存的一组规则。栈中的所有数据都必须占用已知且固定的大小,在编译时大小未知或大小可能变化的数据,需要存储在堆上,所有权主要目的就是管理堆数据。所有权规则:① Rust 中的每个值都有所有者(owner);② 每个值在任意时刻有且仅有一个所有者;③ 当所有者离开作用域之后,该值将被丢弃。

1
2
3
4
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid

所有权(Ownership)机制让 Rust 无需 GC 就能保证内存安全。GC 是运行时根据可达性分析回收内存,而所有权是在编译时确定变量的作用域,当内存的所有者变量离开作用域之后,相应内存就可以被释放。当变量离开作用域时,Rust 会自动调用 drop 函数来释放内存,类似 C++ 的 RAII 机制。

字符串 String 的数据存储在堆上,字符串变量包含指向堆中数据的指针、数据的长度和容量,其存储在栈上。使用 let s2 = s1; 只会复制引用(浅拷贝),为避免 s1s2 离开作用域之后,释放相同内存两次(调用 drop 函数),该语句执行之后 Rust 会使 s1 无效,不需要在其离开作用域之后回收内存,该操作被称为移动(move)。

1
2
3
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!"); // error[E0382]: borrow of moved value: `s1`

当给已有值的变量赋新值时,Rust 会立即调用 drop 释放原始值的内存。如果想要执行深拷贝,可以使用 clone 函数。如果类型实现 Copy 特征,则旧变量在被赋值给其他变量之后仍然有效。不能为实现 Drop 特征的类型实现 Copy 特征。

1
2
3
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");

References and Borrowing

引用(reference)类似指针,它是一个地址,但是和指针不同,Rust 保证引用指向某个特定类型的有效值。传递引用值不会转移所有权,所以可以在 main 中继续使用 s1。函数 calculate_length 中的局部变量 s 是引用类型,其不拥有值的所有权,所以离开作用域之后,其指向的值也不会被回收,所以创建引用的行为被称为借用(borrowing)。

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
s.len()
}

默认不能通过引用 & 修改指向的值,除非声明的是可变引用 &mut,只能对可变变量(使用 let mut 声明)创建可变引用。如果变量有不可变引用,则原变量可读但不可写。如果变量有可变引用,则原变量不可读写。如果变量有一个可变引用,则该变量不能有其他引用,不论是否可变。以上机制可以保证在编译时就避免数据竞争(data race),因为最多有一个线程可以修改变量,而且此时其他线程都无法读取该变量。

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("hello");
change(&mut s);
}

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

引用的作用域从声明的地方开始直到最后一次使用为止,下面的不可变引用 r1r2 的最后一次使用,发生在 r3 的声明之前,所以不会发生编译错误。

1
2
3
4
5
6
7
8
9
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.

let r3 = &mut s; // no problem
println!("{r3}");

Rust 编译器保证数据不会在其引用之前离开作用域,即不允许悬垂引用,否则会发生编译错误。在其他语言中,C++ 存在悬垂引用,Java 中没有原始指针类型不存在这个问题,Go 中的变量生命周期由可达性决定而不是作用域,通过逃逸分析局部变量会在堆上分配,所以也可以避免悬垂引用。

1
2
3
4
5
6
7
8
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String { // error[E0106]: missing lifetime specifier
let s = String::from("hello");
&s // error[E0515]: cannot return reference to local variable `s`
}

The Slice Type

切片(slice)是引用类型,不拥有值的所有权。切片仅由指向数据的指针和长度组成,不像 Go 中还包含容量字段。字符串切片的索引必须位于有效的 UTF-8 字符边界上,如果从多字节字符的中间位置创建切片,则会产生运行时错误。

1
2
3
4
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];

字符串切片的类型有 &str&mut str,区别在于指向的数据是否可变。使用 let mut 声明变量,只是表示可以修改变量绑定的切片,和切片数据是否可变无关。字符串字面量的类型是 &str,所以是不可变的,如果想要获取可变字符串切片,需要使用 String 结合 mut

1
let mut s = "Hello, world!";
1
2
let mut s = String::from("hello");
let slice = &mut s[..];

Defining and Instantiating Structs

在初始化结构体时,如果字段名称和初始化参数相同,则可以省略。如果要使用旧实例的大部分值创建新实例,可以使用结构体更新语法(struct update syntax),.. 表示剩余未显式设置的字段和给定旧实例具有相同的值。该语法类似使用 = 进行赋值,所以未实现 Copy 特征的类型会被转移所有权,旧实例的相应字段会失效。

1
2
3
4
5
6
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
1
2
3
4
5
6
7
8
fn main() {
// --snip--

let user2 = User {
email: String::from("another@example.com"),
..user1
};
}

可以定义元组结构体(tuple struct),元组结构体的字段没有名称只有类型。即使两个元组结构体有相同类型的字段,它们也是不同的类型。可以解构元组结构体,但是需要指定结构体的类型。

1
2
3
4
5
6
7
8
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
let Point(x, y, z) = origin;
}

没有任何字段的结构体被称为类单元结构体(unit-like struct)。如果在结构体中存储引用类型 &str,而不是自身拥有所有权的 String 类型,就需要使用生命周期,否则会发生编译错误。

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}
1
2
3
4
5
6
struct User {
active: bool,
username: &str, // error[E0106]: missing lifetime specifier
email: &str, // error[E0106]: missing lifetime specifier
sign_in_count: u64,
}

Method Syntax

方法的第一个参数必须是 self,表示调用该方法的结构体实例,&self 实际上是 self: &Self 的缩写。在 impl 块中,Self 类型是 impl 块类型的别名,在下面就是 Rectangle。方法可以选择不可变借用 &self、可变借用 &mut self 或获取所有权 self。方法的名称可以和结构体的字段相同(Java 中也可以,Go 中不行)。当调用方法时,Rust 会自动根据方法签名添加 &&mut*,所以不需要显式转换。

1
2
3
4
5
6
7
8
9
10
struct Rectangle {
width: u32,
height: u32,
}

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

impl 中定义的函数被称为关联函数(associated function),其第一个参数不是 self(类似 Java 中的静态方法)。调用关联函数需要使用 Rectangle::square(x) 形式,表示函数 square 位于结构体 Rectangle 的命名空间中。每个结构体可以有多个 impl 块,不过必须和结构体在相同的 crate 中。

1
2
3
4
5
6
7
8
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}

Enums and Pattern Matching

Defining an Enum

枚举类型的字段被称为变体(variant),可以将数据附加到变体上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
1
2
3
4
5
6
7
8
enum IpAddr {
V4(String),
V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

每个变体附加数据的类型和数量可以不同,可以使用 impl 为枚举类型定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

impl Message {
fn call(&self) {
// method body would be defined here
}
}

let m = Message::Write(String::from("hello"));
m.call();

The match Control Flow Construct

可以使用变量名称之后跟 => 表示通配模式,放在最后以匹配其他情况,如果不使用变量则可以使用 _ => 形式。

1
2
3
4
enum Option<T> {
None,
Some(T),
}
1
2
3
4
5
6
7
8
9
10
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

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

Concise Control Flow with if let and let else

1
2
3
4
5
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
1
2
3
4
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
1
2
3
4
5
6
7
8
9
10
11
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};

if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}

Common Collections

Storing Lists of Values with Vectors

1
2
3
4
5
6
7
8
9
10
let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
println!("The third element is {third}");

let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
1
2
3
4
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

Storing UTF-8 Encoded Text with Strings

Rust 中有两种字符串,字符串切片 str 和字符串 String,它们使用的都是 UTF-8 编码,String 是对 Vec<u8> 的封装。使用 + 运算符拼接 String 字符串,会获取第一个参数的所有权,以及第二个参数的引用,类似下面的 add 函数。

实际上,&s2 会被转换为 &s2[..],从而满足 add 函数的 &str 参数类型,该技术被称为 dref coercion。s3 会获取 s1 的所有权,然后将 s2 复制到其末尾。也可以使用 format! 宏执行字符串拼接,该宏不会获取任何参数的所有权。

1
2
3
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
1
fn add(self, s: &str) -> String {
1
2
3
4
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");

由于字符串使用 UTF-8 编码,所以不支持使用索引访问,字符串的长度是其包含的字节数而不是字符数。在 Go 语言中,可以直接使用索引,访问的就是对应的字节。在 Rust 中,虽然不能使用索引访问字符串,但是可以使用范围获取字符串切片,前提是范围位于有效的 UTF-8 字符边界上。

1
2
let s1 = String::from("hi");
let h = s1[0]; // error[E0277]: the type `str` cannot be indexed by `{integer}`
1
2
let hello = "Здравствуйте";
let s = &hello[0..4];

使用 chars() 方法遍历字符,bytes 方法遍历字节。

1
2
3
4
5
6
for c in "Зд".chars() {
println!("{c}");
}
for b in "Зд".bytes() {
println!("{b}");
}

Storing Keys with Associated Values in Hash Maps

在使用 insert 时,对于实现 Copy 特征的类型,其值会复制到哈希表中,而拥有所有权的类型,其值会被移动到哈希表中。如果将引用插入哈希表,则引用指向的值,必须至少和哈希表的生命周期一样长(避免悬垂引用)。哈希表使用 SipHash 哈希函数,可以抵御涉及哈希表的拒绝服务攻击,允许用户指定其他哈希函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

for (key, value) in &scores {
println!("{key}: {value}");
}

Error Handling

Unrecoverable Errors with panic!

panic 表示不可恢复的错误,在遇到 BUG 或显示调用 panic! 宏时产生。当出现 panic 时,程序默认会执行展开(unwinding),这意味着 Rust 会回溯函数栈并清理函数的数据。另一种选择是直接终止(abort),不清理数据就退出程序,此时程序所使用的内存由操作系统清理。

Recoverable Errors with Result

可以使用闭包和 unwrap_or_else 方法替代 match 来简化代码。使用 unwrap 方法,如果 Result 值是 Ok,则返回其中的值,否则会调用 panic!expect 方法类似,只是可以自定义 panic! 的错误信息。

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}
1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
1
2
3
4
5
6
7
8
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}

Generic Types, Traits, and Lifetimes

Rust 在编译时对泛型代码单态化(monomorphization)来保证效率,即为使用的具体类型生成对应类型的代码。在其他语言中,C++ 的做法类似,而 Java 执行类型擦除。

1
fn largest<T>(list: &[T]) -> &T {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<T> {
x: T,
y: T,
}

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

impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

fn main() {
let p = Point { x: 5, y: 10 };

println!("p.x = {}", p.x());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<X1, Y1> {
x: X1,
y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}

fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };

let p3 = p1.mixup(p2);

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

Traits: Defining Shared Behavior

孤儿规则(orphan rule):只有在特征或类型至少有一个属于当前 crate 时,才能对类型实现特征。也就是说,可以为内部类型实现外部特征、为外部类型实现内部特征,但是不能为外部类型实现外部特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}

pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}

impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
1
2
3
pub fn notify(item: &(impl Summary + Display)) {

pub fn notify<T: Summary + Display>(item: &T) {
1
2
3
4
5
6
7
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

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

Validating References with Lifetimes

Rust 中每个引用都有生命周期(lifetime),通常生命周期可以被隐式地推断出来,否则需要显式写出。生命周期的主要目标是避免悬垂引用,Rust 编译器使用借用检查器(borrow checker)比较作用域,确保所有的借用都是有效的。rx 的生命周期分别被标记为 'a'b,编译器会发现 r 引用的变量 x 的生命周期比自身更小,这会导致悬垂引用,所以编译器会报错。

1
2
3
4
5
6
7
8
9
10
fn main() { // error[E0597]: `x` does not live long enough
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
1
2
3
&i32        // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

生命周期注解语法用于将函数参数和返回值的生命周期相关联,生命周期也是泛型。以下函数签名表示,泛型生命周期参数 'a 的具体生命周期是 xy 中生命周期的较小者,返回值不能在生命周期 'a 结束之后使用。由于下面的 resulty 生命周期结束之后访问,所以会引发编译错误。

1
2
3
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
1
2
3
4
5
6
7
8
9
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str()); // error[E0597]: `string2` does not live long enough
}
println!("The longest string is {result}");
}

结构体可以存储引用类型,不过此时需要使用生命周期注解。下面注解意味着,ImportantExcerpt 实例不能在其字段 part 的生命周期结束之后使用。

1
2
3
struct ImportantExcerpt<'a> {
part: &'a str,
}

生命周期省略规则(lifetime elision rules):① 为每个引用参数分配不同的生命周期;② 如果只有一个输入生命周期参数,则将其生命周期赋给所有输出生命周期参数;③ 如果输入生命周期参数有 &self&mut self,则将 self 的生命周期赋给所有输出生命周期参数。如果应用规则之后,仍没有计算出引用的生命周期,则会引发编译错误。

1
2
3
4
5
6
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}

静态生命周期 'static 表示能够在程序运行期间存活,所有字符串字面量都具有静态生命周期。

1
let s: &'static str = "I have a static lifetime.";
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() { x } else { y }
}

Functional Language Features: Iterators and Closures

Closures: Anonymous Functions That Capture Their Environment

闭包(closures)是可以保存在变量中或作为参数传递给其他函数的匿名函数,和函数不同,它允许捕获其被定义时所在作用域中的值。通常不需要为闭包的参数和返回值声明类型注解,编译器可以推断出类型。必须调用 add_one_v3add_one_v4 闭包才能通过编译,编译器会根据调用上下文推断类型,对相同闭包使用不同的类型会引发编译错误。

1
2
3
4
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 ;

闭包有三种捕获变量的方式:不可变借用、可变借用和获取所有权。前两种方式是隐式推断的,获取所有权使用 move

1
2
3
4
5
6
7
8
9
10
use std::thread;

fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");

thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}

闭包可以执行:将捕获的值移出闭包、既不移动也不修改值、

闭包捕获和处理值的方式会影响闭包实现哪些特征:FnOnceFnMutFn 特征分别在获取所有权、可变借用和不可变借用时实现。它们之间是父子关系(和继承无关,只是指定依赖关系),FnOnce <- FnMut <- Fn。如果闭包不需要从环境中捕获值,可以直接使用函数而不是闭包,例如在 Option<Vec<T>> 值上调用 unwrap_or_else(Vec::new)

1
2
3
4
5
6
7
8
9
10
11
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
1
2
3
4
5
6
7
8
9
impl<T> [T] {
pub fn sort_by_key<K, F>(&mut self, mut f: F)
where
F: FnMut(&T) -> K,
K: Ord,
{
stable_sort(self, |a, b| f(a).lt(&f(b)));
}
}

Processing a Series of Items with Iterators

iter 方法生成不可变引用的迭代器,iter_mut 生成可变引用的迭代器,into_iter 生成获取所有权的迭代器。

1
2
3
4
5
6
7
pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;

// methods with default implementations elided
}
1
2
3
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);

Python 基础

参考官方文档The Python Tutorial

An Informal Introduction to Python

除法运算 / 总是返回浮点数,使用 // 向下取整,Java 中的整数除法是向零取整。使用 ** 计算幂,** 的优先级比 - 更高。交互模式下,上次输出的表达式值被赋给变量 _,最好将其视为只读类型,显式为其赋值会创建同名的局部变量,屏蔽内置变量的行为。

1
2
>>> 15 / 3, 11 / 4, 11 // 4, -(3**2), (-3) ** 2
(5.0, 2.75, 2, -9, 9)

字符串使用单引号 '...' 或双引号 "..." 表示,两者没有区别。可以使用三重引号跨行编写字符串,换行符会被包含在字符串中,可以在行尾添加 \ 避免该行为。字符串可以使用 + 合并,也可以使用 * 重复,相邻字符串字面量会自动合并。字符串可以使用索引访问,索引越界会报错,但是切片会自动处理索引越界。字符串是不可变的,要生成不同的字符串应该创建字符串。使用内置函数 len() 获取字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> # 3 times 'un', followed by 'ium'
>>> 3 * 'un' + 'ium'
'unununium'
>>> 'Py' 'thon'
'Python'
>>> text = ('Put several strings within parentheses '
... 'to have them joined together.')
>>> text
'Put several strings within parentheses to have them joined together.'
>>> word = 'Python'
>>> word[42]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
>>> word[4:42]
'on'
>>> word[0] = 'J'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> 'J' + word[1:]
'Jython'
>>> len(word)
6

列表可以使用 + 合并,列表是可变的可以使用 list.append() 方法添加元素。使用 = 将列表赋值给变量是浅拷贝,该变量引用该列表。列表允许包含不同类型的元素,且可以相互嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
>>> squares = [1, 4, 9, 16, 25]
>>> squares + [36, 49, 64, 81, 100]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> rgb = ["Red", "Green", "Blue"]
>>> rgba = rgb
>>> id(rgb) == id(rgba)
True
>>> a = ['a', 'b', 'c']
>>> n = [1, 2, 3]
>>> x = [a, n]
>>> x
[['a', 'b', 'c'], [1, 2, 3]]

Python 使用缩进组织语句,同一块语句要使用相同大小的缩进,可以使用空格或制表符。

1
2
3
4
5
6
>>> a, b = 0, 1
>>> while a < 1000:
... print(a, end=',')
... a, b = b, a+b
...
0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,

More Control Flow Tools

if 语句由 ifelifelse 组成。for 语句由 forin 组成,使用 range() 函数生成等差数列。组合使用 range()len() 实现按索引迭代序列,或者使用 enumerate() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy: Iterate over a copy
for user, status in users.copy().items():
if status == 'inactive':
del users[user]

# Strategy: Create a new collection
active_users = {}
for user, status in users.items():
if status == 'active':
active_users[user] = status
1
2
3
4
5
6
>>> list(range(5, 10))
[5, 6, 7, 8, 9]
>>> list(range(0, 10, 3))
[0, 3, 6, 9]
>>> list(range(-10, -100, -30))
[-10, -40, -70]
1
2
3
4
5
6
7
8
9
>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
... print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb

forwhile 循环可以有对应的 else 子句,如果循环在未执行 break 的情况下结束,则会执行该 else 子句。pass 语句不执行任何操作,当语法上需要一个语句,而程序不需要执行任何操作时,可以使用该语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, 'equals', x, '*', n//x)
... break
... else:
... # loop fell through without finding a factor
... print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
1
2
3
>>> while True:
... pass # Busy-wait for keyboard interrupt (Ctrl+C)
...

match 语句用于模式匹配,使用 _ 通配符匹配剩余情况,使用 | 将多个字面值组合到一个模式中。模式之后可以使用 if 子句,如果判断结果为假,则 match 会继续尝试匹配下一个 case 块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y

match points:
case []:
print("No points")
case [Point(0, 0)]:
print("The origin")
case [Point(x, y)]:
print(f"Single point {x}, {y}")
case [Point(0, y1), Point(0, y2)]:
print(f"Two on the Y axis at {y1}, {y2}")
case _:
print("Something else")
1
2
3
4
5
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")

使用 def 定义函数,如果函数内第一个语句是字符串,则该字符串是文档字符串。函数中的所有变量赋值,都会将值存储在局部符号表中。函数中查找变量引用的顺序依次是,当前函数的局部符号表,外层函数的局部符号表,全局符号表,内置名称表。所以尽管可以引用全局变量和外层函数变量,但是赋值会创建本地变量,除非使用 global/nonlocal 语句定义该变量。函数参数是值传递的,值是对象的引用(地址)。

1
2
3
4
5
6
7
8
9
10
11
12
>>> def fib(n):    # write Fibonacci series less than n
... """Print a Fibonacci series less than n."""
... a, b = 0, 1
... while a < n:
... print(a, end=' ')
... a, b = b, a+b
... print()
...
... # Now call the function we just defined:
... fib(2000)
...
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

函数定义将函数名和函数对象关联,存储在当前符号表中。可以使用其他名称指向相同函数对象。没有 return 语句的函数返回 None,通常解释器会隐藏单个 None 值,可以使用 print() 函数打印。

1
2
3
4
5
>>> fib
<function fib at 0x000001B7F0AA1120>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89
1
2
3
>>> fib(0)
>>> print(fib(0))
None

可以为函数参数指定默认值,默认值在函数定义时求值。默认值只计算一次,当默认值是对象时,多次调用之间会共享该对象。如果是可变对象,则之后的调用会被之前的影响,如果不想共享默认值则需要显式处理。

1
2
3
4
5
6
7
8
9
>>> i = 5
...
... def f(arg=i):
... print(arg)
...
... i = 6
... f()
...
5
1
2
3
4
5
6
7
8
9
10
11
>>> def f(a, L=[]):
... L.append(a)
... return L
...
... print(f(1))
... print(f(2))
... print(f(3))
...
[1]
[1, 2]
[1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def f(a, L=None):
... if L is None:
... L = []
... L.append(a)
... return L
...
... print(f(1))
... print(f(2))
... print(f(3))
...
[1]
[2]
[3]

可以使用 *args 表示可变参数,该参数是元组类型。可以使用 *** 执行解包。

1
2
3
4
5
>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args)) # call with arguments unpacked from a list
[3, 4, 5]
1
2
3
4
5
6
7
8
9
>>> def parrot(voltage, state='a stiff', action='voom'):
... print("-- This parrot wouldn't", action, end=' ')
... print("if you put", voltage, "volts through it.", end=' ')
... print("E's", state, "!")
...
... d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
... parrot(**d)
...
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

使用 lambda 表达式创建匿名函数。可以为函数添加注解,注解被存储在函数的 __annotations__ 属性中。

1
2
3
4
5
6
7
8
>>> def make_incrementor(n):
... return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43
1
2
3
4
5
>>> f = make_incrementor(42)
>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
1
2
3
4
5
6
7
8
9
10
>>> def f(ham: str, eggs: str = 'eggs') -> str:
... print("Annotations:", f.__annotations__)
... print("Arguments:", ham, eggs)
... return ham + ' and ' + eggs
...
... f('spam')
...
Annotations: {'ham': <class 'str'>, 'eggs': <class 'str'>, 'return': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

Data Structures

列表推导式的方括号 [] 中包含以下内容:一个表达式,一个 for 子句,之后是零个或多个 for/if 子句。使用 zip() 函数在多个迭代器上并行迭代,每个迭代器返回一个元素组成元组。使用 del 语句删除列表元素。

1
2
3
4
5
6
7
8
9
>>> combs = []
... for x in [1,2,3]:
... for y in [3,1,4]:
... if x != y:
... combs.append((x, y))
...
... combs
...
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
1
2
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
1
2
3
4
5
6
7
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

元组由逗号分隔的值组成,元组是不可变的,但可以包含可变对象,此时可以修改该可变对象。空元组使用 () 创建,单元素元组使用该元素之后跟 , 逗号创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples may be nested:
>>> u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples are immutable:
>>> t[0] = 88888
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # but they can contain mutable objects:
>>> v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])
1
2
3
4
5
6
7
8
>>> empty = ()
>>> singleton = 'hello', # <-- note trailing comma
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)

可以使用花括号或 set() 函数创建集合,创建空集合只能使用 set() 而不能使用 {}{} 创建的是空字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket) # show that duplicates have been removed
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket # fast membership testing
True
>>> 'crabgrass' in basket
False

>>> # Demonstrate set operations on unique letters from two words
>>>
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a # unique letters in a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # letters in a but not in b
{'r', 'd', 'b'}
>>> a | b # letters in a or b or both
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # letters in both a and b
{'a', 'c'}
>>> a ^ b # letters in a or b but not both
{'r', 'd', 'b', 'm', 'z', 'l'}

字典的键必须是不可变类型,使用 {} 创建空字典,获取不存在键的值会报错。使用 list(d) 获取键列表,使用 sorted(d) 获取排序之后的键列表,使用 innot in 判断键是否存在,使用 dict() 函数创建字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel)
['jack', 'guido', 'irv']
>>> sorted(tel)
['guido', 'irv', 'jack']
>>> 'guido' in tel
True
>>> 'jack' not in tel
False

遍历字典的键值对使用 items() 方法,遍历序列的索引和元素使用 enumerate() 函数,并行遍历使用 zip() 函数,反向遍历使用 reversed() 函数,排序遍历使用 sorted() 函数。

1
2
3
4
5
6
>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
... for k, v in knights.items():
... print(k, v)
...
gallahad the pure
robin the brave
1
2
3
4
5
6
>>> for i, v in enumerate(['tic', 'tac', 'toe']):
... print(i, v)
...
0 tic
1 tac
2 toe
1
2
3
4
5
6
7
8
>>> questions = ['name', 'quest', 'favorite color']
... answers = ['lancelot', 'the holy grail', 'blue']
... for q, a in zip(questions, answers):
... print('What is your {0}? It is {1}.'.format(q, a))
...
What is your name? It is lancelot.
What is your quest? It is the holy grail.
What is your favorite color? It is blue.

使用 isis not 比较两个对象是否是同一个对象。比较运算支持链式操作,a < b == ca < b and b == c 等价。andornot 表示与或非,相当于 Java 中的 &&||!。布尔值有 TrueFalse,使用大写字母开头。在表达式内部赋值需要使用 :=,避免误用 =。布尔运算中使用普通值而不是布尔值时,短路运算的返回值是最后一个已求值的参数。

1
2
3
4
>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'

Classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> def scope_test():
... def do_local():
... spam = "local spam"
...
... def do_nonlocal():
... nonlocal spam
... spam = "nonlocal spam"
...
... def do_global():
... global spam
... spam = "global spam"
...
... spam = "test spam"
... do_local()
... print("After local assignment:", spam)
... do_nonlocal()
... print("After nonlocal assignment:", spam)
... do_global()
... print("After global assignment:", spam)
...
... scope_test()
... print("In global scope:", spam)
...
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

可以为类定义 __init__() 方法,在创建对象之后自动执行。实例对象作为第一个参数隐式传递给方法,obj_name.foo() 相当于 ClassName.foo(obj_name),该参数通常被命名为 self,也可以被命名为其他名称,self 没有特殊含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog:

kind = 'canine' # class variable shared by all instances

def __init__(self, name):
self.name = name # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'

可以动态给对象添加属性或方法,可以在类之外定义函数,然后将其赋值给类属性,毕竟 self 没有特殊含义。在 Python 中,所有值都是对象,都有相应的类型,类型信息存储在 object.__class__ 中。

1
2
3
4
5
6
7
8
9
10
11
>>> class Warehouse:
... purpose = 'storage'
... region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
1
2
3
4
5
6
7
8
9
10
11
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)

class C:
f = f1

def g(self):
return 'hello world'

h = g

Python 中的方法都是 virtual 方法(动态绑定),属性默认是公有的。不存在私有变量,不过约定以单下划线 _ 开头的变量应该被视为私有的,不论是函数、方法或属性。在类定义中,名称改写(name mangling)会将至少带有两个前缀下划线的标识符 __update 替换为 _Mapping_update,避免子类重写方法影响父类中相应方法的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)

def update(self, iterable):
for item in iterable:
self.items_list.append(item)

__update = update # private copy of original update() method

class MappingSubclass(Mapping):

def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)

再见 2023

时间过得好快!今年大概有 5 个月的时间都在搞算法,然后其他时间基本上每周都会打比赛,虽然对面试来说不应该花费这么多时间,但是我喜欢算法给我带来的反馈,以及比赛时能够高度集中注意力的状态。特别是绞尽脑汁然后 AC 的感觉,真的很棒。即使不会做,赛后也可以通过题解来学习。

力扣基本上可以稳定三题,上 2300 分之后就有点没状态,排名波动有点大。在打 CF 之前就听说,力扣分减 700 大概就是 CF 分,结果还真是这样。CF 思维题较多,前四题基本上不会使用很复杂的数据结构,如果能稳定四题就能到 1900 分吧。AtCoder 题目有点水,对我来说,基本上从 D 题开始才算正式进入比赛,但是经常简单题也没做出来。令人印象深刻的是,获得两次群主发的进步奖。

5 月份阅读完《OSTEP》,9、11 月份学习完课程 CMU 15-445,并且做完四个相关的项目,感觉还不错。12 月份阅读《DDIA》25%,然后摸鱼。6-8 月份,高数、线代、Linux、Java 虚拟机、Spring、设计模式雨露均沾,基本上没有特别深入的,当时有点急躁。总的来说,确实有做不少事,但是效率不高,目的不明确,缺乏实践,要做的事还有很多。