Skip to content

所有权

所有权(ownership) 是 Rust 能够在 不依赖垃圾回收(GC) 的前提下保证 内存安全 的关键机制。

IMPORTANT

  • rust 中每个值在任意时刻都只能有一个所有者
  • 当所有者超出作用域时, rust 会自动调用释放内存函数drop(类似其他语言的手动释放内存函数free)
  • 值得所有权可以转移(move), 也可以通过借用(borrow)不获取所有权来临时使用

移动(move)

rust 隐含的设计: 永远不会自动创建数据的"深层"副本(但是可以使用 clone() 手动深拷贝)

rust
let s1 = String::from("hello");
let s2 = s1; // move: s1被移动(move)到s2; s1 失效; 并不是拷贝堆上的数据给s2;

// 编译失败: s1 变量失效
println!("{s1}, world!");

// 下面的所有权属于语句本身
String::from("world!") // 没有变量绑定, 语句结束后没有变量持有, 堆内存会被回收

let s1 = String::from("hello");

s1 变量本身在栈上分配, 由 3 部分组成(ptr,len,cap), 这 3 个元数据存储在栈(stack)上, 而不是堆(heap)上, 但指针所指向的数据可能会是分配在堆上.

  • ptr: 一个指向(存储字符串内容的)内存的指针.(当前String::from("hello")会被在堆内存上分配)
  • len: 当前实际存储的长度.
  • cap: 底层分配的最大长度.

let s2 = s1;

  1. 当把 s1 赋值给 s2 时,数据被复制,此处复制的是栈数据(s1 的 ptr,len,cap),而不是复制 ptr 所指向的堆上的数据.
  2. 然后, rust 会使变量 s1 失效(所以这不是浅拷贝,因为 s1 失效),失效也就意味着不需要释放堆上的数据(已经 move 给 s2 了, 所有权在 s2).

以上过程被称为移动(move)

copy

实现了 copy trait的类型 , 赋值时是值的复制

比如 rust 中的基本类型(Primitive Types: 整型,浮点,字符,布尔)

复合类型(Compound Types: 元组,数组)中的元素如果都是基本类型, 也会实现 copy trait

rust
let s1 = 5;
let s2 = s1; // copy, 而不是move

println!("{s1}"); // 可以正常打印s1, 不会编译错误

clone

集合类型(Collection Types: String,Vec<T>,HashMap<K, V>,struct等),又称动态复合类型,无法确定大小, 一般分配在堆上(变量本身赋值给另一个变量时的动作会是 move;函数调用不传引用也会 move)

struct 中的字段类型如果都是基本类型, 可以实现 copy 而不 move

rs
#[derive(Debug, Clone, Copy)] // copy的实现依赖于clone
struct MoveCommand {
    x: i32,
    y: i32,
}

fn main() {
    let mc = MoveCommand { x: 123, y: 123 };

    let mc2 = mc; // copy, 发生值的复制, 和clone的效果一样, 其实就是深拷贝

    println!("mc: {:?}", mc) // 编译通过, mc变量 依然有效
}

move

rs
#[derive(Debug)] // 没有派生 copy trait
struct MoveCommand {
    x: i32,
    y: i32,
}

fn main() {
    let mc = MoveCommand { x: 123, y: 123 };

    let mc2 = mc; // move, 因为没有实现 copy trait, 赋值时发生的是 move 操作

    println!("mc: {:?}", mc) // 编译失败, move后 mc 失效
}
rs
#[derive(Debug)] // 没有派生 copy trait
struct MoveCommand {
    x: i32,
    y: i32,
}
fn foo(mc: MoveCommand) {
    println!("x is {}", mc.x)
}

fn main() {
    let ix = MoveCommand { x: 456, y: 456 };

    foo(ix); // `MoveCommand`没有派生copy trait, 是move, 不是复制

    println!("ix: {:?}", ix) // 编译失败, ix被移动到函数, ix变量本身已经失效(foo调用结束,ix被drop)
}

struct 中的字段不全是基本类型

rs
#[derive(Debug, Clone)] // 派生clone
struct MoveCommand {
    x: String,
    y: i32,
}

fn main() {
    let mc = MoveCommand {
        x: String::from("hello"),
        y: 123,
    };

    let mc2 = mc.clone(); // clone

    println!("mc: {:?}", mc) // 编译通过, 因为是clone, 所有权没有转移
}

作用域和赋值

将一个 全新的值 赋给一个 已存在的变量 时,Rust 会立即调用 drop函数并释放原始值的内存。(将原始值的内存返还给分配器)

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

println!("{s}, world!");

借用(borrow)

为了避免 move(所有权转移), rust 提供 借用(borrow) 机制, 使用 引用(reference) 来临时访问一个值(不拥有值得所有权)

IMPORTANT

  • 在同一时间,要么有多个不可变引用 (&T),要么只能有一个可变引用 (&mut T)。(避免 double free; 避免数据竞争;)

引用和解引用

引用(&T, &mut T)本身是简单的指针,大小固定,本质是简单类型,但所引用的数据可能是复杂类型。

访问引用的数据需要先解引用, 但 rust 一般会自动解引用, 解引用使用*

rs
fn main() {
    // 创建对动态数组vec的引用
    let v = &vec![10; 40];

    // rust自动解引用, 其实就是 let x = (*v)[3];
    let x = v[3];
    println!("x: {:?}", x)
}
rs
fn largest<T>(list: &[T]) -> &T
where
    T: PartialOrd,
{
    // 访问引用类型中的数据需要先解引用, 因为函数返回值类型是 `&T`, 所以我们先解引用,然后取得下标0的元素, 然后再引用.
    let mut max_value = &((*list)[0]);

    // 一般写法, rust自动解引用, list[0]其实是 (*list)[0];
    // 因为返回值类型是&T, 解引用后取得值后需要再使用"&"引用
    let mut max_value2 = &list[0];


    for item in &(*list) {
        if item > max_value {
            max_value = item;
        }
    }
    max_value
}

问: 既然引用是简单类型,所以可以多个(&T),但为什么不能多个(&mut T),

答: 因为 rust 就是这么设计的, 用来防止数据竞争. “在同一时间,要么有多个不可变引用 (&T),要么只能有一个可变引用 (&mut T)。”

可变引用

重要的限制:如果你有一个指向值的可变引用,那么你不能有其他任何指向该值的引用。

带来的好处: rust 拒绝编译存在数据竞争的代码来避免数据竞争带来的问题

NLL 非词法生命周期

版本 Rust 2015 edition 使用的是词法作用域驱动, 生命周期严格绑定变量作用域(持续整个作用域);

版本 Rust 2018 edition 之后默认启用的特性: Non-Lexical Lifetimes(NLL,非词法生命周期)

作用: 改进了借用检查器, 精细的分析引用的实际使用范围(而不是词法块), 生命周期只会持续到"最后一次使用"的位置,而不是整个词法作用域

以下代码正常通过编译, 如果没有 NLL, 代码会因为借用冲突无法编译. 有了 NLL, 有效减少了编译报错: "同时存在可变,不可变借用"

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem

    let r3 = &mut s; // no problem

    println!("{r3}"); // 编译通过
}
rs
fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    // 编译失败: 后面需要用到不可变引用r1,r2; 此处又创建了可变引用
    // 编译器不允许同时存在(防止读取数据被修改,避免数据竞争,保证数据一致性)
    let r3 = &mut s; 

    println!("{}, {}, and {}", r1, r2, r3);
}

切片(slice)

切片是一种引用类型,因此它没有所有权。让你引用一个集合或数组中连续的元素序列。

rs
fn main() {
    // 使用不可变数组创建不可变切片
    let arr = [10; 6];
    let slice = &arr;
    let slice2 = &arr[1..4];

    // 使用可变数组创建可变切片
    // 可变数组: 可变指的是数组内元素可变, 数组长度在编译时固定(不可变),
    // 数组存储在栈内存上
    let mut arr2 = [10; 6];
    let slice_mut1 = &mut arr2;
    slice_mut1[0] = 99;
    println!("{:?}", arr2);
}
rs
fn main() {
    // 使用动态数组vec(集合类型)创建切片
    let v = vec![10; 6];
    let slice_vec = &v[1..4];
}