第三章 Ownership与结构体、枚举

1. Rust的内存管理模型

1.1 内存管理模型

常见语言的内存管理模型:

  • C/C++: 手动

  • Python/Java/C#: GC自动回收

  • Rust: The Rust Compiler,在编译期间发现问题

  • Stop the World:

是与垃圾回收相关的术语,它指的是在进行垃圾回收时系统暂停程序的运行

这个术语主要用于描述一种全局性的暂停,即所有应用线程都被停止,以便垃圾回收器能够安全地进行工作。这种全局性的停止会导致一些潜在的问题,特别是对于需要低延迟和高性能的应用程序

需要注意的是,并非所有的垃圾回收算法都需要"stop the world", 有一些现代的垃圾回收器采用了一些技术来减小全局停顿的影响, 比如并发垃圾回收和增量垃圾回收

1.2 C/C++ 内存错误

  1. 内存泄漏(Memory Leaks)
C++ 复制代码
int* ptr = new int;
// 忘记释放内存
// delete ptr;
  1. 悬空指针(Dangling Pointers)
c++ 复制代码
int* ptr = new int;
delete ptr;
// ptr 现在是悬空指针
  1. 重复释放(Double Free)
c++ 复制代码
int* ptr = new int;
delete ptr;
delete ptr;
  1. 数组越界(Array Out of Bounds)
c++ 复制代码
int arr[5]
arr[5] = 10;
  1. 内存泄漏(Memory Leaks)
c++ 复制代码
int* ptr
*ptr = 10; // 野指针
  1. 使用已经释放的内存(Use After free)
c++ 复制代码
int* ptr = new int;
delete int;
*ptr = 10;
  1. 堆栈溢出(Stack Overflow)

递归导致的堆栈溢出

  1. 不匹配的 new/delete 或 malloc/free

1.3 Rust内存管理模型

  1. 所有权系统(Ownership System)

  2. 借用(Borrowing)

    • 不可变引用(不可变借用)
    • 可变引用(可变借用)
  3. 生命周期(Lifetimes)

  4. 引用计数(Reference Counting)

rust 复制代码
fn get_length(s: String) {
    println!("String: {}", s.len());
    // 函数结束后的 main::s2 也销毁了
}

fn main() {
    // copy
    let c1 = 1;
    let c2 = c1;
    println!("{c1}"); // c1 依然存在
    // move
    let s1 = String::from("Value");
    let s2 = s1;
    // println!("{s1}");  // error: s1 的 ownership 已经转移给 s2
    let _s3 = s2.clone();
    println!("{s2}"); // 深拷贝是可以的
    get_length(s2);
    // println!("{s2}"); // error: 传入函数之后, s2又无法使用了, 已经被函数借用了 ownership

    let back = first_word("hello world");
    println!("{}", back);
}

fn dangle() -> String {
    "hello".to_owned()
}

// 静态的生命周期
fn dangle_static() -> &'static str {
    "static dangle"
}

// String 与 &str vec u8 ref
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[..]
} 

2. String与&str

2.1 定义

  • String是一个堆分配的可变字符串类型
rust 复制代码
pub struct String {
  vec: Vec<u8>
}
  • &str是指字符串切片引用,是在栈上分配的
    • 不可变引用,指向存储在其他地方的 UTF-8 编码的字符串数据
    • 由指针和长度构成

2.2 如何选择

注意String是具有所有权的,而 &str 并没有

  • Struct中属性使用String

    • 如果不使用显示声明生命周期无法使用 &str
    • 不只是麻烦,还有更多的隐患
  • 函数参数推荐使用&str (如果不想交出所有权)

    • &str 为参数,可以传递 &str 和 &String
    • &String 为参数,只能传递 &String,不能传递 &str
rust 复制代码
struct Person {
    name: String,
    color: String,
    age: i32
}

struct Person2<'a> {
    name: &'a str // 字面量的生命周期和结构体的生命周期一致, 标注为a
}

// &String &str
fn print_str(data: &str) {
    println!("{}", data);
}

// 只能传 &String
fn print_string(data: &String) {
    println!("{}", data);
}

fn main() {
    // 字面量转换成String
    // 1. String::from("xxx")
    // 2. to_string()
    // 3. to_owned()
    let string_value = String::from("Value C++");
    let literal_to_string = "Rust".to_owned(); 

    let new_string_value = string_value.replace("C++", "Cpp");

    println!("{string_value} {literal_to_string} {new_string_value}");

    // &str
    let str_value = "\x52\x75\x73\x74"; // ascii
    println!("{str_value}");

    let color = "green".to_string();
    let name = "John".to_string();
    let people = Person {
        name: name,
        color: color,
        age: 89
    };

    let str_name = "John";
    let people2 = Person2 {
        name: str_name
    };
    let string_name = "John".to_string();

    print_str(&string_name);
    print_str(str_name);

    print_string(&string_name);
}

3. 枚举与匹配模式

3.1 枚举

  • 枚举(enums)是一种用户自定义的数据类型,用于表示具有一组离散可能值的变量

    • 每种可能值都称为"variant" (变体)
    • 枚举名::变体名
  • 枚举的好处

    • 可以使你的代码更严谨、更易读
    • More robust programs
  • 枚举内嵌类型

ps: 我是写kotlin&&Java的, Rust这个内嵌类型相当于Java/Kotlin中的密封类的职能, 能够携带不同的数据, 并且能够进行模式匹配

rust 复制代码
enum Shape {
  Circle(f64),
  Rectangle(f64, f64),
  Square(f64)
}

常见的枚举类型: Option 和 Result

rust 复制代码
pub enum Option<T> {
  None,
  Some(T)
}

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

3.2 匹配模式

  1. match 关键字实现
  2. 可以覆盖所有的变体
  3. 可以用 _、..=、三元(if)等来进行匹配
rust 复制代码
match number {
  0 => println!("Zero"),
  1 | 2 => println!("One or Two"),
  3..=9 => println!("From three to Nine"),
  n if n%2 == 0 => println!("Event number"),
  _ => println!("Other")
}

3.3 代码示例

rust 复制代码
// 最普通的枚举, 没有任何内嵌数据类型
enum Color {
    Red,
    Yellow,
    Blue,
    Black
}

enum BuildingLocation {
    Number(i32),
    Name(String),
    Unkonwn
}

impl BuildingLocation {
    fn print_location(&self) {
        match self {
            BuildingLocation::Number(c) => println!("building number: {c}"),
            BuildingLocation::Name(str) => println!("building name: {str}"),
            _ => println!("unknown")
        }
    }
}

fn print_color(my_color: Color) {
    match my_color {
        Color::Red => println!("red"), 
        Color::Yellow => println!("yellow"),
        Color::Blue => println!("blue"),
        _ => ()
    }
}

fn main() {
    print_color(Color::Red);

    let buiding = BuildingLocation::Name("Leitorz's house".to_string());
    buiding.print_location();
}

4. 结构体、方法、关联函数、关联变量

4.1 结构体简介

结构体是一种用户定义的数据类型,用于创建自定义的数据结构。

rust 复制代码
struct Point {
  x: i32,
  y: i32
}

每条数据(x和y)称为属性(字段field),通过点(.)来访问结构体中的属性

4.2 结构体中的方法

这里的方法是指,通过实例调用(&self、&mut self、self)

rust 复制代码
impl Point {
  fn distance(&self, other: &Point) -> f64 {
    let dx = (self.x - other.x) as f64;
    let dy = (self.y - other.y) as f64;
    (dx * dx + dy * dy).sqrt();
  }
}

4.3 结构体中的关联函数

关联函数是与类型相关联的函数,调用时为结构体名::函数名

rust 复制代码
impl Point {
  fn new(x: u32, y: u32) -> Self {
    Point {x, y}
  }
}

4.4 结构体中的关联变量

这里的关联变量是指,和结构体类型相关的变量,也可以在特质或是枚举中

rust 复制代码
impl Point {
  const PI: f64 = 3.14;
}

调用时使用Point::PI

4.5 代码示例

rust 复制代码
enum Flavor {
    Spicy,
    Sweet,
    Fruity
}

struct Drink {
    flavor: Flavor,
    price: f64
}

fn print_drink(drink: &Drink) {
    match drink.flavor {
        Flavor::Spicy => println!("spicy"),
        Flavor::Sweet => println!("sweet"),
        Flavor::Fruity => println!("fruity")
    }
    println!("{}", drink.price);
}

impl Drink {
    // 关联变量
    const MAX_PRICE: f64 = 10.0;
    // 不可变借用, 不能改变self的值
    fn buy(&self) {
        if self.price > Drink::MAX_PRICE {
            println!("无法购买");
        } else {
            println!("购买成功");
        }
    }
    // 关联函数
    fn new_fruity_drink(price: f64) -> Self {
        Drink {
            flavor: Flavor::Fruity,
            price: price
        }
    }
}

fn main() {
    let sweet_drink = Drink {
        flavor: Flavor::Sweet,
        price: 16.0
    };
    print_drink(&sweet_drink);
    sweet_drink.buy();

    let fruity_drink = Drink::new_fruity_drink(10.0);
    print_drink(&fruity_drink);
}

5. Ownership与结构体

5.1 Ownership Rules

  • Each value in Rust has an owner
  • There can only be one owner at a time
  • Values are automatically dropped when the owner goes out of scope

5.2 Value Passing Semantics 值传递语义

每当将一个值从一个位置传递到另一个位置时,borrow checker 都会重新评估所有权

  1. Immutable Borrow 使用不可变的借用,值的所有权仍归发送方所有,接收方直接接收对该值的引用,而不是该值的副本。但是,他们不能使用该引用来修改它指向的值,编译器不允许这样做。释放资源的责任仍由发送方承担。仅当发件人本身超出范围时,才会删除该值。

  2. Mutable Borrow 使用可变的借用,所有权和删除值的责任也由发送者承担。但是接收方能够通过它们接收的引用来修改该值。

  3. Move这是所有权从一个地点转移到另一个地点。borrow checker 关于释放该值的决定将由该值的接收者(而非发送者)通知。由于所有权已从发送方转移到接收方,因此发送方在将引用移动到另一个上下文后不能再使用该引用,发送方在移动后对 value 的任何使用都会导致错误。

5.3 结构体中关联函数的参数

rust 复制代码
&self (self: &Self) 不可变引用

&mut self(self: &mut Self) 可变引用

self(self: Self) // Move

impl Point {
 get (self: Self) -> i32 {
   self.x
 }
}
  • self 参数相当于 Point::get(point) 调用后丧失所有权 (Point为实例对象)

  • &self 参数相当于 Point::get(&point)

  • &mut self 参数相当于 Point::get(&mut point)

5.4 代码示例

rust 复制代码
struct Counter {
    number: i32
}

impl Counter {
    fn new(number: i32) -> Self {
        return Self { number };
    }

    // 不可变借用
    fn get_number(&self) -> i32 {
        return self.number;
    }

    // 可变借用
    fn modify_number(&mut self, new_number: i32) {
        self.number = new_number;
    }

    // 释放
    fn release(self) {
        println!("free {}", self.number);
    }

    // 合并
    fn combine(c1: Self, c2: Self) -> Self {
        Self { number: (c1.number + c2.number) }
    }
}

fn main() {
    let mut counter = Counter::new(10);
    let number = counter.get_number();
    println!("{number}");

    counter.modify_number(12);
    let new_number = counter.get_number(); // 可变借用, 改变number后也没有丧失所有权
    println!("{new_number}");

    counter.release(); // move后无法使用

    let c1 = Counter::new(1);
    let c2 = Counter::new(2);
    let c3 = Counter::combine(c1, c2); // 合并后, c1 和 c2 丧失了所有权
    println!("{}", c3.get_number());
}

6. 堆与栈、Copy与More

6.1 堆与栈

Stack:

  • 堆栈将按照获取值的顺序存储值,并以相反的顺序删除值
  • 操作高效,函数作用域就是在栈上
  • 堆栈上存储的所有数据都必须具有已知的固定大小数据

Heap:

  • 堆的规律性较差,当你把一些东西放到你请求的堆上时,你请求,请求空间,并返回一个指针,这是该位置的地址
  • 长度不确定

6.2 Box

Box是一个智能指针,它提供对堆分配内存的所有权。它允许你将数据存储在堆上而不是栈上,并且在复制或移动时保持对数据的唯一拥有权。使用Box可以避免一些内存管理问题,如悬垂指针和重复释放。

  1. 所有权转移
  2. 释放内存
  3. 解引用
  4. 构建递归数据结构
rust 复制代码
struct Point {
    x: i32,
    y: i32
}

fn main() {
    let box_point = Box::new(Point {x: 20, y: 20}); // 将结构体分配在堆上

    let mut boxed_point = Box::new(32);

    println!("{}", *boxed_point);

    *boxed_point += 10;
}

6.3 Copy与Clone

Move: 所有权转移

Clone: 深拷贝

Copy: Copy是在Clone的基础上建立的marker trait(Rust中最类似继承的关系)

  1. trait(特质)是一种定义共享行为的机制。Clone也是特质

  2. marker trait 是一个没有任何方法的 trait, 它主要用于向编译器传递某些信息, 以改变类型的默认行为

  • stack:

    • 基础类型
    • tuple和array
    • struct与枚举等也是存储在栈上 如果属性有String等在堆上的数据类型会有指向堆的
  • heap:

    • Box
    • Rc
    • String/Vec等

一般来说在栈上的数据类型都默认 copy 但 struct 等默认为 move, 需要 Copy 只需要设置数据类型实现 Copy 特质即可, 或调用 Clone 函数(需要实现 Clone 特质)

rust 复制代码
// 这里的 `#[derive(...)]` 是一个属性宏。它告诉编译器为 `Book` 这个结构体自动实现括号里的这些 trait
#[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 y = x.clone();
    println!("{x}");

    let b1 = Book {
        page: 1,
        rating: 0.1
    };
    let b2 = b1; // 默认调用的move
    println!("{:?}", b1); 
}
相关推荐
alwaysrun12 小时前
Rust中所有权和作用域及生命周期
rust·生命周期·作用域·所有权·引用与借用
FleetingLore1 天前
Rust | str 常用方法
rust
一只小松许️2 天前
深入理解 Rust 的内存模型:变量、值与指针
java·开发语言·rust
m0_480502643 天前
Rust 登堂 之 Cell 和 RefCell(十二)
开发语言·后端·rust
烈风4 天前
011 Rust数组
开发语言·后端·rust
Dontla4 天前
Turbopack介绍(由Vercel开发的基于Rust的高性能前端构建工具,用于挑战传统构建工具Webpack、vite地位)Next.js推荐构建工具
前端·rust·turbopack
开心不就得了5 天前
构建工具webpack
前端·webpack·rust
ftpeak6 天前
《WebAssembly指南》第九章:WebAssembly 导入全局字符串常量
开发语言·rust·wasm
红烧code6 天前
【Rust GUI开发入门】编写一个本地音乐播放器(15. 记录运行日志)
rust·gui·log·slint