Skip to main content

所有权

它有个独特的特性,就是没有 GC 就可以保证内存安全。

什么是所有权?

  • 所有程序在运行时都必须管理它们使用计算机内存的方式
    • NodeJS 垃圾收集机制,在程序运行时,就会不断地寻找不再使用的内存,老生代和新生代的内存。
    • C/C++,程序员必须显式地分配和释放内存。
    • Rust 通过所有权系统管理内存,其中包含一组编译器在编译时检查的规则。当程序运行时,所有权特性不会减慢程序的运行速度。
tip

Rust 的核心特性就是所有权。

Stack VS Heap

  • Stack

    • 把值压到 Stack 上不叫分配。
    • 储存在 Stack 上的数据必须是已知的固定大小的,如果想要实际数据,我们必须使用指针来定位。
    • 把数据压到 Stack 上要比在 Heap 上分配快得多,因为 Heap 多了一个指针跳转的环节,属于间接的访问。
      • 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在 Stack 的顶端。
    • LIFO 后进先出。
  • Heap

    • 未知数据大小 未知运行时大小的数据放在 Heap 内存。
    • 把数据存在 Heap 时,会请求一定数量的空间。
    • 操作系统在 Heap 找到一块足够大的空间,并且标记为在用,而且返回一个指针,它就是这个空间的地址。
    • 这个过程叫做在 Heap 上进行分配,有时候仅仅成为 “分配”。
  • 为什么 Stack 比 Heap 快

      1. 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远在 Stack 的顶端。
      1. Heap 上分配空间需要做更多的工作:
      • 操作系统首先需要找到一块足够大的空间来存储数据,然后要做好记录方便下次分配。
      1. 访问 Heap 的数据要比访问 Stack 中的数据慢,因为需要通过指针才能找到 Heap 中的数据。
      • 现代处理器由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。
      1. 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(Stack)。
      1. 如果数据存放的距离比较远,那么处理器的处理速度就会慢(Heap)。
      • Heap 上分配大量的空间也是需要时间的。
  • 函数调用

当你的代码调用函数时,值被传入到函数(也包括指向 Heap 的指针)。函数本地的变量被压到 Stack 上。当函数结束后,这些值会从 Stack 上弹出。

提示

在 Rust 这样的系统级编程语言里,一个值是在 stack 上还是在 heap 上对语言的行为和你为什么要做某些决定是有更大影响的。

所有权的规则

  • 每个值都有一个变量,这个变量是该值的所有者。
  • 每个值同时只能有一个所有者。
  • 当所有者超出作用域(Scoped)的时候,该值将被删除。

变量作用域

  • Scope 就是程序中一个项目的有效范围。

fn main(){
// name 不可用
let name = "ACE"; // name 可用
// 可以操作 name
} // 作用域结束 name 不再可用

所有权存在的原因

  • 所有权解决的问题:
    • 跟踪代码的哪些部分正在使用 Heap 的哪些数据。
    • 最小化 Heap 上的重复数据量。
    • 清理 Heap 上未使用的数据以避免空间不足。
tip

如果懂了所有权就不会去想存在 Heap 还是 Stack 上了。

所有权的移动案例

fn test_fn(){
let str = String::from("hello world");
str // 函数在返回值的过程中也会发生所有权转移
}

let test = test_fn(); // 这就是所有权由 test_fn 作用域转移到了 test

提示

💡 注意:当一个包含 Heap 数据的变量离开作用域的时候,它的值就会被 drop 函数清理,除非数据的所有权移动到另一个变量上了。

💡 注意:slice 不含有

Copy

  • 浅拷贝 shallow Copy

  • 深拷贝 deep Copy

  • 你也许会将复制指针、长度、容量视为浅拷贝,但由于 Rust 让 s1 失效了,所以要用到一个新的术语:移动(Move)

  • 原来 s1 指向 箭头右侧的 Heap 内存,然后 s1 赋给 s2,于此同时 s1 就失效了就变成灰色了,并且此时只有 s2 是有效的。
  • 也就是说 s1 被移动到了 s2 身上,既然只有 s2 是生效的,所以只有它离开自己作用域的时候释放内存空间,所以也就不会再发生二次释放内存的可能性了,所以这也就体现出了 Rust 的安全性。
  • 这里也隐含了一个设计原则:Rust 不会自动创建数据的深拷贝。
    • 也就是说,就运行时的性能而言,任何自动赋值的操作都是廉价的。

Clone

  • 如果真想对 Heap 上面的 String 数据进行深度拷贝,而不仅仅是 String 数据进行深度拷贝,而不仅仅是 Stack 上的数据,可以使用 Clone 方法。

fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();

println!("{},{}", s1,s2);
}

这种方式比较消耗资源,主要针对堆上面的数据

复制

针对 Stack 上的数据实际上不需要 Clone ,叫复制就可以了

🌰

fn main(){
let x = 5;
let y = x;
println!("{}, {}", x, y);
}

上面的代码并没有使用 clone 这个方法,在 main 函数里面的第 3 行之后,x,y 都是有效的,这是因为 x 是整数类型,而且整数类型在编译的时候就确定了自己的大小 并且能将自己的数据完整的存储到Stack 上,而对于这些值的复制操作永远都是非常快速的,这也同样的意味着在创建变量外之后我们没有任何理由去阻止变量 x 继续去保持有效。 就是对于这些类型而言深拷贝和浅拷贝是没有任何区别的调用 clone 这个方法并不会与直接的浅拷贝有直接行为上的区别,因此我们不需要在类似的场景考虑上面的问题。

  • Copy Trait(接口),可以用于像整数那样完全存放在 Stack 上面的数据类型
  • 如果一个类型实现了 Copy 这个 Trait,那么旧的变量在赋值后仍然可用
  • 如果一个类型或者该类型的一部分实现了 Drop Trait,那么 Rust 不允许让它再去实现 Copy Trait 了,否则编译就会进行报错了。

一些拥有 Copy Trait 的类型

  • 任何简单标量的组合类型都是可以 Copy 的
  • 任何需要分配内存或者某种资源的都不是 Copy 的
  • 一些拥有 Copy Trait 的类型
    • 所有的整数类型,例如 u32
    • bool
    • char
    • 所有的浮点类型,例如 f64
    • Tuple(元组),如果其所有的字段都是可以 Copy 的,那么它也是拥有 Copy Trait 的类型
      • (i32, i32) 是
      • (i32, String) 不是

所有权 与 函数

  • 在语义上,将值传递给函数和把值赋给变量是类似的:
    • 将值传递给函数将发生移动或者复制
详解函数

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

take_ownership(s); // 这个时候 s 这个值就等于移动到 take_ownership函数 内部了,从这以后 s 就不再有效了。

// println!("{}", s); // 所以这行打开注释就会报错 borrow of moved value: `s`

let x = 5;

makes_copy(x); // 由于 x 是个 i32 类型,而 i32 这个类型实现了 Copy Trait ,所以传递给函数 makes_copy 实际上是 x 的副本,所以 x 在这行之后仍然有效

println!("x:{}", x);
} // 在这一行 s 和 x 也就离开了作用域全部失效,但是由于之前 s 发生了移动,所以不会发生其他的事情

fn take_ownership(some_string: String) {
println!("{}", some_string);
} // 在这一行 Rust 会自动调用 drop 这个函数,some_string 所占用的内存就被释放了

fn makes_copy(some_number: i32 // 这个 some_number 参数实际上是源数据的副本) {
println!("{}", some_number);
} // 在这一行 Rust 会自动调用 drop 这个函数,some_number 所占用的内存就被释放了

返回值 与 作用域

函数在返回值的过程中同样也会发生所有权的转移


fn main() {
//
let s1 = gaves_ownership();

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

let s3 = takes_and_gives_back(s2);
}

fn gaves_ownership() -> String {
let some_string = String::from("hello");
some_string
} // some_string 作为一个返回值移动至了 调用它的函数里面,也就是 main 函数里面, 实际上就是把 some_string 移动给了 s1

fn takes_and_gives_back(a_string: String) -> String {
a_string
} // 这个函数的作用就是获取了 string 的所有权并将它作为结果进行返回,而这个函数的返回值又被移动到了 s3 上


  • 一个变量的所有权总是遵循同样的模式:
    • 把一个赋值给其它变量时就会发生移动
    • 当一个包含 heap 数据的变量离开作用域时,它的值就会被 drop 函数清理,除非数据的所有权移动到另一个变量上了才不会被清理。

切片

Rust 的另外一种不持有所有权的数据类型:切片(slice)

注意

fn main() {
let s = String::from("Hello World");
let wordIndex = first_world(&s);
// s.clear();

println!("{}", wordIndex);
}

fn first_world(s: &String) -> usize {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
println!("{}", i);
println!("{}", item);
if item == b' ' {
return i;
}
}

s.len()
}

  • 如果调用了 s.clear 方法把字符串清空。由于 wordIndex 和 字符串 s 本身没任何关联,所以即使这个字符串被清空了,那么 wordIndex 值仍然是 5,所以它就毫无意义,而此时我们 想用 wordIndex 来提取字符串第一个单词就会发生错误引起 BUG。 所以 first_world 这样的函数设计必须随时关注 wordIndex 的有效性,需要确保索引 和 String 变量 s 他们之间 的同步性,所以这类工作往往是相当繁琐,而且特别容易出错,而针对这类问题 Rust 就提供了解决方案 字符串切片

字符串切片

是指向字符串中一部分内容的引用

  • 形式:[开始索引..结束索引]

    • 开始索引就是切片其实位置的索引值
    • 结束索引是切片终止位置的下个索引值

图中的 s 就是个 string 类型,箭头左边它是放在 Stack 上的,内容是放在 Heap 上面的也就是箭头右边,但是上图有点问题, len 长度和 capacity 容量应该是 11 才对, world 就是个字符串的切片 从 6 切到 11。


fn main() {
let s = String::from("Hello World");

let hello = &s[..5];
let world = &s[6..];

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

let whole = &s[0..s.len()];
let whole1 = &s[..];

println!("{} {}", whole, whole1);
}

注意
  • 字符串切片的范围索引必须发生在有效的 UTF-8 字符边界内
  • 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出。

fn main() {
let s = String::from("Hello World");

let wordIndex = first_world(&s);

s.clear();

println!("{}", wordIndex);
}

fn first_world(s: &String) -> &str {
let bytes = s.as_bytes();

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

&s[..]
}

// 错误提示

Compiling lesson5 v0.1.0 (D:\Rust\lesson5)
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
--> src\main.rs:6:5
|
2 | let s = String::from("Hello World");
| - help: consider changing this to be mutable: `mut s`
...
6 | s.clear();
| ^^^^^^^^^ cannot borrow as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `lesson5` due to previous error

字符串字面值

字符串字面值实际上就是切片

  • 字符串字面值被直接存储在二进制程序中
  • let s = "Hello World";
  • 变量 s 的类型是 &str,它是一个指向二进制程序特定位置的切片
    • &str 是不可变引用,所以字符串字面值也是不可变的

将字符串切片作为参数传递

  • fn first_world(s: &String)-> &str {}
  • 有经验的 Rust 工程师会采用 &str 作为参数类型,因为这样就可以同时接收 String&str 类型的参数了;
  • fn first_world(s: &str)-> &str {}
    • 使用字符串切片,直接调用该函数
    • 如果传入的是 String 类型的参数,可以创建一个完整的 String 切片来调用该函数
  • 定义函数时使用字符串切片来代替字符串引用会使我们 API 更加通用,且不会损失任何功能

fn main() {
let my_string = String::from("Hello World");
let wordIndex = first_world(&my_string[..]);

let my_string_literal = "Hello World";
let wordIndex = first_world(my_string_literal);

println!("{}", wordIndex);
}

fn first_world(s: &str) -> &str {
let bytes = s.as_bytes();

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

  • 数组切片
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

// 这个跟字符串切片一样,这个里面存储了一个指针,这个指针指向了起始元素的位置,他还存储一个长度,长度是2
}