https://inasa.dev/blog/rss.xml

Rust | Ownership

2023-06-20

一段不安全的代码

int* foo() {
    int a;          // 变量a的作用域开始
    a = 100;
    char *c = "xyz";   // 变量c的作用域开始
    return &a;
}                   // 变量a和c的作用域结束
  • 变量 ac 都是局部变量,函数结束后将局部变量 a 的地址返回,但局部变量 a 存在栈中,在离开作用域后,a 所申请的栈上内存都会被系统回收,从而造成了 悬空指针(Dangling Pointer) 的问题。

​ 虽然编译可以通过,但是运行的时候会出现错误。

  • 变量 c常量字符串,存储于常量区, "xyz" 只有当整个程序结束后系统才能回收这片内存。

栈(Stack)与堆(Heap)

  • 栈中的所有数据都必须占用已知且固定大小的内存空间
  • 对于大小未知或者可能变化的数据,需要存储在堆上
    • 当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)
    • 接着,该指针会被推入中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据
  • 写入
    • 入栈比在堆上分配内存要快
    • 入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可
    • 在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备
  • 读取
    • 得益于 CPU 高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在 10 倍以上
    • 栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中
    • 访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存

所有权原则

  • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

变量绑定背后的数据交互

转移所有权

let x = 5;
let y = x;

两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存

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

对于基本类型(存储在栈上),Rust 会自动拷贝,但是 String 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。

String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。

String 类型指向了一个堆上的空间,这里存储着它的真实数据, 下面对上面代码中的 let s2 = s1 分成两种情况讨论:

  • 拷贝 String 和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是 String 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响
  • 只拷贝 String 本身 这样的拷贝非常快,因为在 64 位机器上就拷贝了 8字节的指针8字节的长度8字节的容量,总计 24 字节,但是带来了新的问题:一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:s1s2

当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。

s1s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误。

所以,Rust 这样解决问题:s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了

看看在所有权转移后再来使用旧的所有者

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);

Rust 会禁止你使用无效的引用

move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
fn main() {
    let x: &str = "hello, world";
    let y = x;
    println!("{},{}",x,y);
}

x 只是引用了存储在二进制中的字符串 "hello, world",并没有持有所有权。

因此 let y = x 中,仅仅是对该引用进行了拷贝,此时 yx 都引用了同一个字符串。

克隆(深拷贝)

Rust 永远也不会自动创建数据的 “深拷贝”。任何自动的复制都不是深拷贝。

如果确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

s2 完整的复制了 s1 的数据

拷贝(浅拷贝)

浅拷贝只发生在栈上

Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用。

任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy。如下是一些 Copy 的类型:

  • 所有整数类型
  • 布尔类型
  • 所有浮点数类型
  • 字符类型
  • 元组,当且仅当其包含的类型也都是 Copy 的时候
  • 不可变引用 &T

引用与解引用

常规引用是一个指针类型,指向了对象存储的内存地址。

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

不可变引用

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}

通过 &s1 语法,我们创建了一个指向 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。

可变引用

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。

可变引用同时只能存在一个

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作用域在这里结束
  // 新编译器中,r3作用域在这里结束

引用作用域的结束位置从花括号变成最后一次使用的位置,因此 r1 借用和 r2 借用在 println! 后,就结束了,此时 r3 可以顺利借用到可变引用。

悬垂引用(Dangling References)

指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。

fn dangle() -> &String { // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

解决方法是直接返回 String

fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

String所有权被转移给外面的调用者


  • https://course.rs/basic/ownership/ownership.html
  • https://course.rs/basic/ownership/borrowing.html