Rust 学习 05

archive time: 2022-05-12

好久没更新博客了, 再学学 Rust 罢

所有权

这算是 Rust 的标志之一了, 这也是 Rust 的内存安全的保障

  • 内存通过一个所有权系统来管理, 包含一组编译器在编译时检查的规则
  • 当程序运行时, 所有权特性不会减慢程序的运行速度

Stack 内存

Stack 保持 LIFO (last in first out) 的规则存储数据

存数据的方式为 push ( 压栈 ), 取数据的方式为 pop ( 弹栈 )

而且在 Stack 上的数据大小是固定的或者已知的

Heap 内存

编译时大小未知的或者会改变大小的数据都会存放在 Heap 上

  • 把数据放入 Heap 时会请求一定数量的空间
  • 操作系统会在 Heap 中找到一块足够大的空间来存放, 并将其标记为占用, 返回该地址指针

该操作被称为 内存分配

对比

  • 把值放到 Stack 上不叫做分配
  • 因为指针的大小是固定的, 所以指针是可以放在 Stack 上的
    • 但是要取到具体的值需要指针定位
  • 数据压到 Stack 上要比在 Heap 上分配要快
    • Stack 是直接在顶端存放数据, 无需寻找合适空间
    • Heap 会寻找合适大小的空间来存放数据
  • 访问 Stack 要比 Heap 快, 因为要找 Heap 数据需要用指针来寻找

所有权可以跟踪正在使用的数据, 而且可以最小化 Heap 上重复数据量, 并且可以清理 Heap 上未使用的数据

规则

  1. 每个值都有个对应变量, 该变量为该值所有者 (Owner)
  2. 每个值同时只能有一个所有者
  3. 所有者超出作用域时, 该值将会被删除 (未被使用)

作用域 (Scope) 是对象 (item, object) 有效的范围

fn main() {
    // s 不可用
    let s = "hello"; // s 可用
    // s 作用域范围
} // s 不再可用, 对应值将被删除

String 类型

String 比基础标量类型 (scalar type 不可再分类型) 更复杂

  • 字符串字面值 (literal) 是不可变的
  • String 类型在 Heap 上分配, 可以存放未知数量的文本
let s1 = "hello"; // 字面值
let s2 = String::from("hello"); // String 类型

String 类型是可以修改的

fn main() {
    let mut s = String::from("Hello");
    s.push_str(", World");
    println!("{}", s); // => Hello, World
} // drop 函数

变量和数据交互

移动

两个 5 被压入了 Stack 中

let x = 5;
let y = x;

对于 String 类型

let s1 = String::from("Hello"); // ptr, len, capacity
let s2 = s1; // s2 有和 s1 一样的值, 所以 ptr 指向同一个字符串
// s1 失效
// println!("{}", s1); // 错误 borrow of moved value

在 Java 里, 这种没有复制值的情况被称为 浅拷贝 (shallow copy)

但是 Rust 还让 s1 失效了, 故这种行为被称为 移动 (move)

clone

如果想要进行深拷贝, 则可以使用 clone 方法

let s1 = String::from("Hello"); // ptr, len, capacity
let s2 = s1.clone(); // 连 Heap 数据也复制了一份
println!("{}, {}", s1, s2); // 正常

复制

移动 和 clone 是针对于 Heap 元素说的

对于 Stack 元素执行的是复制, 深拷贝和浅拷贝是一样的

如果实现了 Copy trait, 则旧变量在赋值后仍可用

如果实现了 Drop trait, 则无法实现 Copy trait

简单的标量组合类型都是实现了 Copy 的

而需要分配内存或某种资源的都没实现 Copy 的

所有权与函数

可以通过这个例子来看看

fn main() {
    let s = String::from("hello");
    take_ownership(s);

    // println!("{}", s); // value borrowed here after move

    let x = 5;
    make_copy();

    println!("x: {}", x);
} // 释放 x

fn take_ownership(some_string: String) {
    println!("{}", some_string);
} // drop some_string

fn make_copy(some_number: i32) {
    println!("{}", some_number);
}

引用和借用

如果只想使用其值而不获取其所有权, 可以使用引用 (ref)

fn main() {
    let s = String::form("hello");
    let len = length(&s); // 传入引用

    println!("{}: {}", s, len); // Ok
}

fn length(s: &String) -> usize { // 接收引用
    return s.len();
}

把引用作为函数参数的行为被叫做 借用

在特定作用域内对某块数据只能有 一个 可变引用

  • 可以防止数据竞争
    • 两个或多个指针同时访问同一个数据
    • 至少有一个指针用于写入数据
    • 没有使用任何机制来同步 (sync) 数据的访问

但是可以创建新的作用域来允许非同时的创建多个可变引用

let mut s = String::from("hello");

{
    let s1 = &mut s;
}

let s2 = &mut s;

不可以同时有一个可变引用和不可变引用, 但是可以有多个不可变引用

悬空引用 会在编译期被查出

切片

切片指向原来值的一部分

fn main() {
    let mut s = String::from("hello world");
    let wordIndex = first_word(&s[..]);

    // s.clear(); // 不可以把 s 借用为可变
    println!("{}", wordIndex);
}

fn first_word(s: &str) -> &str { // 返回字符串切片类型
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[.. i];
        }
    }

    return &s[..];
}

好, 暂时就这样罢, 固体物理学好难啊