学习 Rust 🦀:07 - 所有权:内存动态

字符串类型:

到目前为止,我们在编译时一直使用已知大小的类型,并将这些类型存储在 Stack 。为了进一步演示 Rust 的所有权,我们必须使用存储在堆中的 类型 。String类型就是其中一种类型 一般来说,指向“分配”给String变量的 Heap 内存块的指针及其长度存储在Stack中, 实际数据存储在 Heap中 。字符串变量在编译时的大小可以是未知的,并且它可以是可变的,因为 - 正如所指出的 - 它在内存中保留了一个可以增长和缩小的位置,这与在最终可执行文件中硬编码的字符串文字不同,因此与已知的字符串文字不同。因此它们存储在 Stack 中。

let mut hello = String::from("Hello"); // A new String variable using the "from" method from the String namespace.
hello.push_str(", World!"); // Appends a string to the same String variable.
println!("{hello}");

内存和分配:

对于像 String 这样在编译时大小未知的类型,我们必须做两件事:

  1. 在运行时请求所需的内存。
  2. 当我们使用完 String 后释放该内存。

请求 部分 由我们完成。当我们在前面的示例中调用“String::from”方法时,我们就这样做了。第二部分是 Rust 所有权发挥作用的地方。对于使用“垃圾收集”机制的语言,GC(垃圾收集器)负责处理这个问题。对于其他语言(Rust 除外),开发人员必须同时完成请求和发布部分,而后者被证明具有挑战性。因为忘记释放内存会浪费内存。太早这样做会导致变量无效。而且执行两次也是一个错误。简而言之,

我们需要将一个分配与一个发布精确配对。

然而,Rust 会代表您释放所请求的内存。每当变量超出范围时它都会这样做。

移动和复制:

内存释放规则看起来很简单,但想象一下这个场景:

let s1 = String::from("String Value");
let s2 = s1;
println!("S2: {s2}");
println!("S1: {s1}");

对于 堆存储类型 (如果我们可以这样称呼它们),类似这样的语句let s2 = s1;不会复制s1堆中的数据并将新副本关联起来,s2因为这在运行时会很昂贵(想象一下一个非常非常大的字符串,它消耗大量部分内存被复制!)。相反,Rust 保持 堆数据 完整,只是在s2. 这就带来了一个问题,一旦变量超出范围,Rust 就会释放分配的内存。因此,假设s1首先超出范围,然后 Rust 释放 堆数据 。不久之后,s2超出了范围,然后 Rust 据说尝试释放已经释放的 堆数据 s1超出了范围。这就是上一节提到的“双重释放”问题。 Rust 在此类情况下强制执行内存安全的做法,s1在前面的示例中在let s2 = s1. 所以println!("S1: {s1}")会导致编译错误。

s1已经搬到了s2。

但这不是 Rust 对整数等简单类型的行为方式!对于编译时大小已知的类型,Rust 不会在像前一个这样的赋值语句中移动变量。它 复制 变量,因为它已经存储在 堆栈 中,这更容易复制。因此,以下示例是有效的:

let n1 = 10;
let n2 = n1;
println!("n1: {n1}"); // is valid.
println!("n2: {n2}");

n1已经被复制到n2

克隆:

但是,如果复制 堆数据 正是我们想要的并且不想 移动 变量怎么办?Rust 有“克隆”方法:

let s3 = String::from("Clone");
let s4 = s3.clone();
println!("s3: {s3}"); // is valid.
println!("s4: {s4}");

此方法克隆堆 数据 (如前所述),这在运行时可能非常昂贵,因此请谨慎使用。

为了保持简短,我必须在这里停下来。在下一篇文章中,我们将讨论所有权和功能!到时候见。

学习Rust🦀:07-所有权:内存动态