笔记
笔记¶
所有权¶
所有权原则¶
- Rust 中 每一个值都被一个变量所拥有,该变量被称为值的所有者;
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者;
- 当所有者(变量)离开作用域范围时,这个值将被丢弃;
变量绑定背后的数据交互¶
// 对于基本类型(存储在栈上),Rust 会自动拷贝,但是 String 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。
// 实际会上,String 类型是一个复杂类型,由存储在栈中的怼指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存。容量是堆内存分配空间的大小,长度是目前已经使用的大小。
// 当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1 和 s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做二次释放的错误。
// 因此 Rust 这样解决问题:当 s1 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了。
// 拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 s1 无效了,因此这个操作被称为移动(move),而不是浅拷贝。
let s1 = String::from("hello");
let s2 = s1;
// 这段代码和之前的 String 有一个本质上的区别:String 的例子中 s1 持有了通过 String::from("hello") 创建的值的所有权,而这个例子中,x 只是引用了存储在二进制克制性文件中的字符串 "hello, world",并没有持有所有权。
// 因此 let y = x 中,仅仅是对该应用进行了拷贝,此时 y 和 x 都应用了同一个字符串。
let x: &str = "hello, world";
let y = x;
println!("{}, {}", x, y);
// Rust 永远也不会自动创建数据的深拷贝。因此,任务自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
可变引用¶
// 引用作用域的结束位置从花括号变成最后一次使用的位置
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中 r1, r2 作用域在这里结束
let r3 =&mut s;
println!("{}", r3);
}
// 老编译器中 r1 r2 r3 作用域在这里结束
复合类型¶
结构体¶
// 我们使用了自身拥有所有权的 String 类型而不是基于引用的 &str 字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
// 你也可以让 User 结构体从其它对象借用数据,不过这么做,就需要引入生命周期这个新概念,简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "someone@example.org",
username: "someone",
sign_in_count: 1,
active: true,
}
}
枚举¶
// 任何类型的数据都可以放入枚举成员中:例子字符串、数值、结构体甚至另一个枚举;
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1, y:1};
let m3 = Message::ChangeColor(255, 255, 0);
}
数组¶
// the trait `Copy` is not implemented for `String`
// 基本类型在 Rust 中赋值是以 Copy 的形式,let array = [3; 5]; 底层就是不断的 Copy 出来的,但是很可惜复杂类型都没有深拷贝,只能一个个创建。
fn main() {
let array = [String::from("hello"); 8];
println!("{:?}", array);
}
fn main() {
let arr: [String; 8] = std::array::from_fn(|_i| String::from("hello"));
println!("{:?}", arr);
}
fn main() {
let one = [1, 2, 3] as [u8; 3];
let two: [u8; 3] = [1, 2, 3];
let blank1 = [0; 3];
let blank2: [u8; 3] = [0; 3];
let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2];
for a in arrays.iter() {
println!("{:?}", a);
for n in a.iter() {
println!("\t{} + 10 = {}", n, n + 10);
}
let mut sum = 0;
for i in 0..a.len() {
sum += a[i];
}
println!("\t({:?} = {})", a, sum);
}
}
模式匹配¶
@ 绑定¶
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// 绑定新变量 p 同时对 Point 进行解构
let p @ Point { x: px, y: py } = Point { x: 10, y: 20 };
println!("p={:?} x={} y={}", p, px, py);
let point = Point { x: 10, y: 5 };
if let p @ Point { x: 10, y: _ } = point {
println!("x=10 p={:?}", p);
} else {
println!("x!=10 p={:?}", p);
}
}
泛型¶
struct Point<X, Y> {
x: X,
y: Y,
}
// 结构体泛型
impl<T, U> Point<T, U> {
// 方法泛型
fn minup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point { x: self.x, y: other.y }
}
}
// 针对特定的具体类型进行方法定义
impl Point<f64, f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p1 = Point { x: 1.1, y: 2.2 };
let distance = p1.distance_from_origin();
let p2 = Point { x: 1, y: 2 };
let p3 = p1.minup(p2);
println!("distance={} p3.x={} p3.y={}", distance, p3.x, p3.y);
}
// [i32; 3] 和 [i32; 2] 是两个完全不同的类型,因此无法用同一个函数调用
fn display_array_i32_3(arr: [i32; 3]) {
println!("{:?}", arr);
}
// 数组切片泛型
fn display_array_slice<T: std::fmt::Debug>(arr: &[T]) {
println!("{:?}", arr);
}
// 值泛型
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr3 = [1, 2, 3];
display_array(arr3);
let arr2 = [4, 5];
display_array(arr2)
}
特征¶
特征定义与实现的位置(孤儿规则):
如果你想要为类型
A
实现特征T
,那么A
或者T
至少有一个是在当前作用域中定义的!
use std::fmt::Display;
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
// 使用特征作为函数参数
pub fn notify_1(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 特征约束
pub fn notify_1_constraint<T: Summary>(item: &T) {}
pub fn notify_2(item: &(impl Summary + Display)) {}
pub fn notify_2_constraint<T: Summary + Display>(item: &T) {}
#[derive(Debug)]
struct Point<T: std::ops::Add<Output = T>> {
x: T,
y: T,
}
impl<T: std::ops::Add<Output = T>> std::ops::Add for Point<T> {
type Output = Point<T>;
fn add(self, p: Self) -> Self::Output {
Point {
x: self.x + p.x,
y: self.y + p.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2;
println!("{:?}", p3);
}
特征对象¶
-
特征对象大小不固定
这是因为,对于特种
Draw
,类型Button
可以实现特征Draw
,类型SelectBox
也可以实现特征Draw
,因此特征没有固定大小。 -
几乎总是使用特征对象的引用方式,如
&dyn Draw
、Box<dyn Draw>
- 虽然特征对象就没有固定大小,但它的应用类型的大小是固定的,它由两个指针组成(
ptr
和vptr
),因此占用两个指针大小。 - 一个指针
ptr
指向实现了特征Draw
的具体类型的实例,也就是当做特征Draw
来用的实例,比如类型Button
的实例、类型SelectBox
的实例。 - 另一个指针
vptr
指向了一个虚表vtable
,vtable
中保存了类型Button
或类型SelectBox
的实例对于可以调用的实现于特征Draw
的方法。当调用方法时,直接从vtable
中找到方法并调用。之所以要使用一个vtable
来保存各实例的方法,是因为实现了特征Draw
的类型有多种,这些类型拥有的方法各不相同,当讲这些类型的实例都当做Draw
特征来使用时(此时,它们全部看作是特征Draw
类型的实例),有必要区分这些实例各自都有哪些方法可调用。
- 虽然特征对象就没有固定大小,但它的应用类型的大小是固定的,它由两个指针组成(
pub trait Draw {
fn draw(&self);
}
pub struct Button {}
impl Draw for Button {
fn draw(&self) {}
}
pub struct SelectBox {}
impl Draw for SelectBox {
fn draw(&self) {}
}
// 这种写法限制了 Screen 实例的 Vec<T> 中的每个元素必须是 Button 类型或者全是 SelectBox 类型。
// 如果只需要同质(相同类型)集合,更倾向于采用泛型 + 特征约束这种写法,因其实现更清晰,且性能更好(特征对象,需要从运行时从 vtable 动态查找需要调用的方法)。
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T: Draw> Screen<T> {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub trait Pilot {
fn fly(&self);
}
pub trait Wizard {
fn fly(&self);
}
struct Human {}
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human {};
// 优先调用类型上的方法
person.fly();
// 调用特征上的方法
Pilot::fly(&person);
Wizard::fly(&person);
}
pub 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());
}
struct Wrapper(Vec<String>);
impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
生命周期¶
- 生命周期标注并不会改变任何引用的实际作用域。(在通过函数签名指定生命周期时,我们并没有改变传入应用或者返回应用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过)。
- 标注的生命周期只是为了取悦编译器,让编译器不要难为我们。
- 例如有一个变量,只能活一个花括号,那么就算你给它标注一个活全局的生命周期,它还是会在前面的花括号结束处被释放掉,并不会真的全局存活。
// 这连个参数 first 和 second 至少活得和 'a 一样久,至于到底活多久或者哪个活得更久,抱歉我们都无法得知。
fn unless<'a>(first: &'a i32, second: &'a i32) {}
生命周期消除规则¶
-
每一个应用参数都会获得独自的生命周期。
-
若只有一个输入生命周期(函数参数中只有一个应用类型),那么该生命周期会被赋予给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期。
-
若存在杜哥输入生命周期,且其中一个是
&self
或&mut self
,则&self
的生命周期被赋给所有的输出生命周期。
'static'
¶
fn get_memory_location() -> (usize, usize) {
// 字符串字面量 "hello world!" 的生命周期是 'static
// 持有它的变量 s 的生命周期是函数作用域
let s = "Hello World!";
let ptr = s.as_ptr() as usize;
let len = s.len();
(ptr, len)
}
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(pointer as *const u8, length)) }
}
fn main() {
let (ptr, len) = get_memory_location();
let s = get_str_at_location(ptr, len);
println!("ptr={ptr:x} len={len} s={s}");
}
错误处理¶
/// Errors:
///
/// - 若 `hello_world.txt` 文件不存在
///
/// ```text
/// Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
/// ```
///
/// - 若用户没有相关的权限访问 `hello_world.txt`
///
/// ```text
/// Error: AppError { kind: "io", message: "Permission denied (os error 13)" }
/// ```
///
/// - 若 `hello_world.txt` 包含有非数字的内容,例如 `Hello, world!`
///
/// ```text
/// Error: AppError { kind: "parse", message: "invalid digit found in string" }
/// ```
fn main() -> Result<(), AppError> {
let mut file = File::open("hello_world.txt")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let number = content.parse::<usize>()?;
Ok(())
}
包和模块¶
受限可见性¶
注释和文档¶
//! Parent module document
pub mod mod_a {
//! Module `mod_a` document
/// Function `add_one` document
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = crate::hello_world::mod_a::add_one(arg);
/// assert_eq!(6, answer);
/// ```
///
/// # Panics
///
/// # Errors
///
/// # Safety
pub fn add_one(x: i32) -> i32 {
x + 1
}
}
格式化输出¶
fn main() {
// 位置参数
// "12"
println!("{}{}", 1, 2);
// 位置参数: 默认顺序
// "12"
println!("{0}{1}", 1, 2);
// 位置参数: 自定义顺序
// "21"
println!("{1}{0}", 1, 2);
// 具名参数
// "a b c"
println!("{a} {b} {c}", a = "a", b = "b", c = "c");
// 对齐
// "Hello x !"
println!("Hello {:5}!", "x");
// 对齐: 位置参数
// "Hello x !"
println!("Hello {:1$}!", "x", 5);
// 对齐: 具名参数
// "Hello x !"
println!("Hello {:width$}!", "x", width = 5);
// 对齐: 填充数字
// "Hello -0010!"
println!("Hello {:05}!", -10);
// 对齐: 左对齐
// "Hello x !"
println!("Hello {:<5}!", "x");
// 对齐: 右对齐
// "Hello x!"
println!("Hello {:>5}!", "x");
// 对齐: 居中对齐
// "Hello x !"
println!("Hello {:^5}!", "x");
// 对齐: 指定填充符号居中对齐
// "Hello ==x==!"
println!("Hello {:=^5}!", "x");
// 精度
// "3.14!"
println!("{:.2}!", 3.1415926);
// "+3.14!"
println!("{:+.2}!", 3.1415926);
// "hello!"
println!("{:.5}!", "hello world");
// 进制
// "0b1010!"
println!("{:#b}!", 10);
// "0b00001010!"
println!("{:#010b}!", 10);
// "0xa!"
println!("{:#x}!", 10);
}
闭包¶
- 所有的闭包都自动实现了
FnOnce
特征,因此任何一个闭包都至少可以被调用一次; - 没有移出所捕获变量的所有权的闭包自动实现了
FnMut
特征; - 不需要对捕获变量进行改变的闭包自动实现了
Fn
特征;
FnOnce
¶
在 Rust 中,闭包的行为取决于它所捕获的变量。如果闭包捕获的是一个可复制类型,则会对其进行赋值,而不是移动。
这是因为闭包需要对其捕获的变量进行所有权管理,以确保在闭包执行期间它们仍然有效。对于可复制类型,复制是最简单的管理方式。
需要注意的是,如果想要在闭包中移动一个值,可以使用 move 关键字来强制将其移动到闭包中。在这种情况下,闭包将获取变量的所有权,并且该变量将不再可用于闭包之外。
- 所有的闭包都实现了
FnOnce
,比如Fn
、FnMut
都可以当做FnOnce
用;- 一个闭包实现了哪种
Fn
特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们;- 所以
FnOnce
也可以不获取变量所有权;- 加上
move
之后的闭包只能是FnOnce
,不能再当做Fn
、FnMut
使用,因为所捕获的变量所有权已经move
闭包中了。
fn main() {
let data_owned = vec![1, 2, 3];
let closure_once = move || {
println!("Owned data: {:?}", data_owned);
};
closure_once();
println!("Outside: {:?}", data_owned);
}
fn fn_once<F: FnOnce(usize) -> bool + Copy>(func: F) {
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z| z == x.len())
}
FnMut
¶
fn main() {
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}
Fn
¶
fn main() {
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}", s, str);
exec(update_string);
println!("{:?}", s);
}
// 以不可变借用的方式捕获环境中的值
fn exec<'a, F: Fn(String) -> ()>(f: F) {
f("world".to_string())
}
闭包作为函数返回值¶
fn factory(x: i32) -> Box<dyn Fn(i32) -> i32> {
if x > 1 {
Box::new(move |y| y + x)
} else {
Box::new(move |y| y - x)
}
}
fn main() {
println!("{}", factory(1)(100));
println!("{}", factory(2)(100));
}
类型转换¶
// 首先,从直觉上来说,该方法会报错,因为 T 没有实现 Clone 特征,但是真实情况是什么呢?
// 1. 首先通过值犯法调用就不再可行,因为 T 没有实现 Clone 特征,也就无法调用 T 的 clone 方法。
// 2. 接着编译器尝试引用方法调用,此时 T 变成 &T,在这种情况下,clone 方法的签名如下:fn clone(&&T) -> &T,接着我们现在对 value 进行了引用。
// 3. 编译器发现 &T 实现了 Clone 特征(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此推出 cloned 也是 &T 类型。
// 4. 最终,我们复制除一份引用指针,这很合理,因为值类型 T 没有实现 Clone,只能去复制一个指针了。
fn do_stuff<T>(value: &T) {
let cloned = value.clone();
}
不定长类型¶
// 每一个特征都是一个可以通过名称来引用的动态大小类型。
// 如果想把特征作为具体的类型来传递给函数,你必须将其转换成一个特征对象:诸如 &dyn Trait 或者 Box<dyn trait> 或者 Rc<dyn Trait> 这些引用类型。
// ?Sized 特征用于表明类型 T 既有可能是固定大小的类型,也可能是动态大小的类型。
// 参数从 T 变成了 &T,因为 T 可能是动态大小的,因此需要用一个固定大小的指针来包裹它。
fn generic<T: ?Sized>(t: &T) {}
智能指针¶
Box 使用场景¶
- 将数据存储在堆上
- 避免栈上数据拷贝
- 将动态大小类型变为
Sized
固定大小类型 - 特征对象
多线程¶
线程屏障¶
use std::sync::{Arc, Barrier};
use std::thread;
fn main() {
let mut handles = Vec::with_capacity(6);
let barrier = Arc::new(Barrier::new(6));
for _ in 0..6 {
let b = barrier.clone();
handles.push(thread::spawn(move || {
println!("[before] thread_id={:?}", thread::current().id());
b.wait();
println!("[after] thread_id={:?}", thread::current().id());
}));
}
for handle in handles {
handle.join().unwrap();
}
}
日志¶
use std::env;
use log::{debug, error, info, trace, warn};
/// Dependencies:
///
/// ```shell
/// cargo add log env_logger
/// ```
fn main() {
env::set_var("RUST_LOG", "trace");
env_logger::init();
error!("error");
warn!("warn");
info!("info");
debug!("debug");
trace!("trace");
}
常见错误¶
闭包碰到特征对象¶
- 特征对象隐式的具有
'static
生命周期。 - 闭包和特征对象的相爱相杀主要原因就在于特征对象默认具备
'static
生命周期,同时,如果一个闭包拥有'static
生命周期,那闭包无法通过引用的方式来捕获本地环境中的变量。如果你非要捕获,只能使用非'static
。