本文属于我的 Rust 学习笔记 系列。来源:原子之音。
视频
代码
Rust 内存管理模型
常见的内存管理模型有三种:
- C/C++:手动管理,效率高但易错
- Java/C#/Python:交给 GC,安全但 STW 伤害性能
- Rust:Onwership、Borrow Checker、Lifetime。编译期做检查,如果发现内存有问题会直接编译不通过,通过所有权机制避免一些问题产生。安全且理论性能接近C,但更难
Stop the world
Stop the world(STW)与垃圾回收(GC)相关,是指在 GC 进行时系统暂停程序的运行。
STW 主要用于描述一种全局性的暂停,即应用所有线程都会停止,以便垃圾回收器安全工作。对于需要低延迟高性能的程序,这种全局性的停止会导致一些潜在的性能问题。
并非所有 GC 算法都会导致 STW,一些现代的算法采用并发或者增量的 GC,减少全局停顿带来的影响。
C 内存错误大全
- 内存泄漏
int* ptr = new int;
- 悬空指针
int* ptr = new int;
delete ptr;
- 重复释放
int* ptr = new int;
delete ptr;
delete ptr;
- 数组越界
int arr[5];
arr[5] = 5;
- 野指针
int* ptr;
ptr = 10;
- Use after free
int* ptr = new int;
delete ptr;
*ptr = 10;
- Stack Overflow
递归溢出
- 不匹配
new/delete malloc/free
Rust 内存管理模型
fn get_length(s: String) -> usize {
println!("String: {}", s);
s.len()
}
fn main() {
let c1 = 1;
let c2 = c1; println!("{}", c1);
let s1 = String::from("value");
let s2 = s1.clone(); println!("{}", s1);
println!("{}", s1.len());
let len = get_length(s2);
println!("{}", len);
let back = first_word("hello world");
println!("{}", back);
let back = first_word("we are the world");
println!("{}", back);
}
fn dangle() -> String {
"hello".to_owned()
}
fn dangle_static() -> &'static str {
"jdkfj"
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
字符串数据类型
String:堆分配的可变字符串类型
pub struct String {
vec: Vec<u8>,
}
&str:字符串字面量,字符串的不可变切片引用,栈上分配,UTF-8 编码,由指针和长度构成
区别:String 有所有权,字面量没有。
- 结构体属性尽量使用 String。
- 如果不使用显式声明生命周期,就无法使用 &str。
- 可能有隐患。
- 函数参数在不想交出所有权的情况下,建议使用 &str
- &str 参数可以传递 &str 和 &String。
- &String 参数只能传递 &String。
例子
struct Person<'a> {
name: &'a str,
color: String,
age: i32,
}
fn print(data: &str) {
println!("{}", data);
}
fn print_string_borrow(data: &String) {
println!("{}", data);
}
fn main() {
let name = String::from("Value C++");
let course = "Rust".to_string();
let new_name = name.replace("C++", "CPP");
println!("{name} {course} {new_name}");
let rust = "\x52\x75\x73\x74"; println!("{rust}");
let color = "green".to_string();
let name = "John";
let people = Person {
name: name,
color: color,
age: 89,
};
let value = "value".to_owned();
print(&value);
print("value");
print_string_borrow(&value);
}
枚举与匹配
枚举
枚举(enum)是自定义的数据类型,表示一组具有离散可能值的变量。
- 每种可能值都成为变体(variant)
- 用法:{枚举名}::{变体名}
枚举可以让代码更严谨易读安全。
枚举支持内嵌类型,使得 rust 表达能力非常强,抽象度非常高。
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Square(f64),
}
常用枚举类型
pub enum Option<T> {
None,
Some<T>,
}
pub enum Result<T, E> {
Ok(T),
Err<E>,
}
匹配模式 match
匹配模式必须覆盖所有变体,可以使用_
、..=
、if
等来实现。
match number {}
0 => println!("zero");
1 | 2 => println!("one or two");
3..=9 => println!("three to nine");
n if n % 2 == 0 => println!("even");
_ => println!("others");
例子
use std::collections::btree_set::Union;
enum Color {
Red,
Yellow,
Blue,
}
fn print_color(my_color: Color) {
match my_color {
Color::Red => println!("Red"),
Color::Yellow => println!("Yellow"),
Color::Blue => println!("Blue"),
}
}
enum BuildingLocation {
Number(i32),
Name(String), Unknown,
}
impl BuildingLocation {
fn print_location(&self) {
match self {
BuildingLocation::Number(c) => println!("building number {}", c),
BuildingLocation::Name(s) => println!("building name {}", *s),
BuildingLocation::Unknown => println!("unknown"),
}
}
}
fn main() {
let a = Color::Red;
print_color(a);
let house = BuildingLocation::Name("fdfd".to_string());
let house = BuildingLocation::Number(1);
let house = BuildingLocation::Unknown;
house.print_location();
}
结构体
结构体是用户自定义的数据类型,可以用于创建各种自定义的数据结构。
struct Point {
x: i32,
y: i32,
}
结构体中的每条数据(如上述 x,y)称为属性(field),可以通过.
运算符来访问结构体中的属性。
下面会通过结构体引入一些概念。这些概念通常会和结构体/特质/枚举一起使用,且定义在类型的impl
中,和类型本身定义分离。
方法
严格来说 Rust 中并不存在方法,只有关联函数。本章概念是为了方便有面向对象语言基础的初学者理解 Rust。
这里的方法是指通过实例调用的关联函数,参数中往往包含 &self(实例引用)、&mut self(可变实例引用)、self(所有权移动)。
impl Point {
fn distance(&self, other: &Other) -> f64 {
let dx = (self.x - other.x) as f64;
let dy = (self.y - other.y) as f64;
(dx * dx + dy * dy).sqrt()
}
}
关联函数
关联函数是与类型相关联的函数,调用时使用类型名::函数名
。
impl Point {
fn new(x: u32, y: u32) -> Self {
Point { x, y }
}
}
关联变量
这里的关联变量是指和类型相关的变量。调用时使用类型名::变量名
。
impl Point {
const PI: f64 = 3.14;
}
例子
enum Flavor {
Spicy,
Sweet,
Fruity,
}
struct Drink {
flavor: Flavor,
price: f64,
}
impl Drink {
const MAX_PRICE: f64 = 10.0;
fn buy(&self) {
if self.price > Drink::MAX_PRICE {
println!("I am poor");
return;
}
println!("buy it");
}
fn new(price: f64) -> Self {
Drink {
flavor: Flavor::Fruity,
price,
}
}
}
fn print_drink(drink: Drink) {
match drink.flavor {
Flavor::Fruity => println!("fruity"),
Flavor::Spicy => println!("spicy"),
Flavor::Sweet => println!("sweet"),
}
println!("{}", drink.price);
}
fn main() {
let sweet = Drink {
flavor: Flavor::Sweet,
price: 6.0,
};
println!("{}", sweet.price);
print_drink(sweet); let sweet = Drink::new(12.0);
sweet.buy();
}
所有权
Rust 所有权总是会涉及到self
,但这个self
和面向对象的this
完全不一样。
所有权规则
- Rust 中每个 value 都有一个所有者。
- 同时只能有一个所有者。
- value 超出作用域时会自动销毁。
因此,每次将值从一个位置传到另一个位置时,borrow checker 都会重新评估所有权。
值传递语义
- 不可变借用(immutable borrow):值的所有权归发送方所有,接收方直接接收对该值的引用而不是副本。接收方不能通过引用来修改它指向的值。释放资源的行为由发送方负责。(&self或self: &Self)
- 可变借用(mutable borrow):值的所有权和释放责任依然由发送方承担,但接收方可以通过接收的引用来修改值。在同一时刻只能有一个可变借用。(&mut self或self: &mut Self)
- 所有权转移(move):值的所有权会直接移交给接收方,发送方在引用移动的上下文之后就不能再使用该引用(报错 borrow of moved value)。(self或self: Self)
上述括号中或后面的self
可以起任意变量名字。
例子
struct Counter {
number: i32,
}
impl Counter {
fn new(number: i32) -> Self {
Self { number }
}
fn get_number(&self) -> i32 {
self.number
} fn add(&mut self, increment: i32) {
self.number += increment;
} fn give_up(self) {
println!("free {}", self.number);
}
fn combine(c1: Self, c2: Self) -> Self {
Self {
number: c1.number + c2.number,
}
}
}
fn main() {
let mut c1 = Counter::new(0);
println!("c1 number {}", c1.get_number());
println!("c1 number {}", c1.get_number());
c1.add(2);
println!("c1 number {}", c1.get_number());
println!("c1 number {}", c1.get_number());
c1.give_up();
let c1 = Counter::new(2);
let c2 = Counter::new(1);
let c3 = Counter::combine(c1, c2);
println!("c3 number {}", c3.get_number());
}
堆栈
堆和栈
- 栈(stack):先入后出
- 按照获取值的顺序存储值,并按照相反顺序删除值
- 高效,函数作用域就在栈上
- 栈上存储的所有数据的大小都必须已知
- 堆(heap):
- 堆是无人管理的自由内存区,规律性较差,需要时手动申请,不需要时手动释放(当然在 Rust 中是自动管理的)
- 长度不固定
基础类型、元组和数组、结构体与枚举都会存储在栈上;Box、Rc、String、集合(Vec等)会指向堆。注意如果结构体、枚举中包含堆上的类型,也会指向堆。
Box
Box 是一个智能指针,提供堆上分配内存的所有权(强制入堆),并且在复制和移动的时候保持数据的唯一所有权。能够有效避免一些内存管理问题。
Box 的用途主要包括所有权转移、释放内存、解引用、构建递归数据结构。
struct Point {
x: i32,
y: i32,
}
fn main() {
let boxed_point = Box::new(Point { x: 10, y: 20 });
println!("x:{}, y:{}", boxed_point.x, boxed_point.y);
let mut boxed_point = Box::new(32); println!("{}", boxed_point); println!("{}", *boxed_point); *boxed_point += 10; println!("{}", *boxed_point); }
拷贝和克隆
- move 所有权转移
- clone 深拷贝,需显式调用clone()方法
- copy 基于 clone 的标记特质
Rust 中特质(trait)是定义共享行为的机制,clone就是特质。而标记特质(marker trait)是一个没有任何方法的特质,表面上没有定义任何行为,实际上是用于向编译器传递的附加信息,以改变类型的默认行为。
一般栈上的类型都默认实现了 copy,但结构体等默认是 move,实现 copy 需要实现 copy 特质,或者实现 clone 特质并调用 clone。
例子
#[derive(Debug, Clone, Copy)]
struct Book {
page: i32,
rating: f64,
}
fn main() {
let x = vec![1, 2, 3, 4];
let y = x.clone();
println!("{:?}", y);
println!("{:?}", x);
let x = "ss".to_string();
let _x = x.clone();
println!("{:?}", x);
let b1 = Book {
page: 1,
rating: 0.1,
};
let b2 = b1; println!("{:?}", b1);
}