2024-02-02
Rust
0
请注意,本文编写于 510 天前,最后修改于 508 天前,其中某些信息可能已经过时。

目录

栈(Stack)与堆(Heap)
所有权
所有权规则
变量作用域
String 类型
内存与分配
变量与数据交互的方式:移动
变量与数据交互的方式:克隆
只在栈上的数据:拷贝
所有权与函数
返回值与作用域
引用与借用
引用
可变引用
悬垂引用
引用的规则
Slice 类型

所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。

所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制(例如 Java),在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存(例如 C)。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

栈(Stack)与堆(Heap)

Rust 中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。

栈中的数据后进先出,可以想象为一端封闭的管道,增加数据叫进栈,移出数据叫出栈,栈中的所有数据都必须占用已知且固定的大小。

堆是缺乏组织的,当向堆放入数据时,需要申请一定大小的空间。

在堆上分配内存(分配):内存分配器在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针。将数据压栈不是分配。

入栈比分配要快,因为入栈无需搜索新的内存空间:其位置总是在栈顶。

访问堆上的数据比访问栈上的数据要慢,因为必须通过指针来访问。

当代码调用函数时,传给函数的值(包括指针)和函数的局部变量被压入栈中,函数结束时,数据出栈。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。

所有权

所有权规则

  1. Rust 中的每一个值都有一个所有者
  2. 值在任何一个时刻有且只有一个所有者;
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

变量作用域

作用域是一个项在程序中有效的范围。

rust
{ // s 在这里无效,它尚未声明 let s = "hello"; // 从此处起,s 是有效的 // 使用 s } // 此作用域已结束,s 不再有效
  • s 进入作用域时,它是有效
  • 一直持续到 s 离开作用域为止

String 类型

字符串字面值是被硬编码进程序里的字符串值,是不可变的。

String 类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本,可以使用 from 函数基于字符串字面值来创建 String

rust
let mut s = String::from("hello"); s.push_str(", world!"); // 在 String 后追加字符串字面值 println!("{s}");

:: 是运算符,允许将特定的 from 函数置于 String 类型的命名空间下

String 可变而字符串字面值不可变,区别在于这两个类型对内存的处理上

内存与分配

对于 String 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这意味着:

  • 必须在运行时向内存分配器请求内存
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法

第一部分:调用 String::from

第二部分:内存在拥有它的变量离开作用域后就被自动释放,Rust 调用了 drop 函数,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop

变量与数据交互的方式:移动

rust
let x = 5; let y = x;

5 绑定到 x;接着生成一个值 x 的拷贝并绑定到 y”。现在有了两个变量,xy,都等于 5。这两个数被放入了栈中。

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

String 由三部分组成,一个指向存放字符串内容内存的指针、一个长度和一个容量,这一组数据存储在栈上。

s1 赋值给 s2 时,String 的数据被复制了,即指针、长度、容量被复制了,指针指向的堆上的数据没有被复制

类似但不同于 JavaScript 中的浅拷贝,因为 s1 被赋值给 s2 后,Rust就认为 s1 不再有效了,因此如果再打印 s1 ,则会被 Rust 禁止使用无效的引用

这个操作在 Rust 中称为移动,这解决了二次释放的错误(两次释放相同内存会导致内存污染,可能会导致潜在的安全漏洞)

Three tables: tables s1 and s2 representing those strings on the stack, respectively, and both pointing to the same string data on the heap. Table s1 is grayed out be-cause s1 is no longer valid; only s2 can be used to access the heap data.

隐含的设计选择:Rust 永远也不会自动创建数据的深拷贝,因此任何自动的赋值都可以被认为是对运行时性能影响较小的

变量与数据交互的方式:克隆

如果确实需要深度复制 String 中堆上的数据,可以使用 clone 函数:

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

出现 clone 时,资源消耗会增加

只在栈上的数据:拷贝

对于在栈上的数据,拷贝其实际的值是快速的,这里没有深浅拷贝的区别

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权与函数

向函数传递值可能会移动或复制,取决于数据类型

rust
fn main() { let s = String::from("hello"); // s 进入作用域 takes_ownership(s); // s 的值移动到函数里,后续不再有效 let x = 5; // x 进入作用域 makes_copy(x); // x 移动到函数里 // x 类型为 i32,是 Copy trait,后续还是有效的 } // x先移出作用域,然后是s,但s已经被移动到函数 takes_ownership 中 fn takes_ownership(str: String) { // str 进入作用域 println!("{}", str); } // str 被移出作用域,调用 `drop` 方法,占用的内存被释放 fn makes_copy(int: i32) { // int 进入作用域 println!("{}", int); } // int 被移出作用域,没有特殊之处

返回值与作用域

返回值也可以转移所有权

rust
fn main() { let s1 = gives_ownership(); // gives_ownership 将返回值移动给 s1 let s2 = String::from("value"); // s2 进入作用域 let s3 = takes_then_gives_back(s2); // s2 移动到 takes_then_gives_back 中,返回值移动给 s3 } // s3 被移出作用域并丢弃,s2 被移出作用域但 s2 已失效,s1 被移出作用域 fn gives_ownership() -> String { let str = String::from("This is a String."); // str 进入作用域 str // str 移动给 main 中的 s1 } fn takes_then_gives_back(str_a: String) -> String { // str_a 进入作用域 str_a // str_a 移动给 main 中的 s3 }

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

如果想要在函数中使用某个值而不需要其所有权(即后续还需要接着使用该值),可以使用元组来返回多个值,以避免原来的变量失效,但过于麻烦和形式主义

rust
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() 返回字符串的长度 (s, length) }

Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用references)。

引用与借用

引用

通过引用 String 的值,避免其被移动到调用的函数中

rust
fn main() { let s1 = String::from("value"); let len = calculate_length(&s1); println!("The length of \"{}\" is: {}", s1, len); } fn calculate_length(s: &String) -> usize { // s 是 String 的引用 s.len() } // s 离开作用域,但其没有引用值的所有权,所以什么也不会发生

传递 &s1calculate_length,同时在函数定义中,获取 &String 而不是 String

& 符号就是引用,允许使用值但不获取所有权

&s1 语法让我们创建一个 指向s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。同理,函数签名使用 & 来表明参数 s 的类型是一个引用。

Three tables: the table for s contains only a pointer to the table for s1. The table for s1 contains the stack data for s1 and points to the string data on the heap.

与使用 & 引用相反的操作是 解引用dereferencing),它使用解引用运算符,*

创建一个引用的行为称为借用,如同现实生活中借来的东西是要还的,我们没有拥有权。借用的变量默认不可修改,如同变量默认不可变一般。

可变引用

将变量用 mut 修饰,在创建函数时和传参时也都使用 mut 修饰,就修改为可变引用

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

可变引用有一个很大的限制:如果已经有一个对该变量的可变引用,就不能再创建对该变量的引用,即不能在同一时间多次将其作为可变变量借用

不能在拥有不可变引用的同时拥有可变引用

rust
let mut s = String::from("hello"); // 不能同时进行可变引用 { let s1 = &mut s; // no problem let s2 = &mut s; // BIG PROBLEM println!("{}", s1); } // 不能在拥有不可变引用的同时拥有可变引用 { let s1 = &s; let s2 = &s; let s3 = &mut s; println!("{}, {}, and {}", s1, s2, s3); } // 变量 s1 在最后一次使用完之后结束,作用域没有重叠,编译器可以在作用域结束之前判断不再使用的引用 { let s1 = &mut s; // no problem println!("{}", s1); let s2 = &mut s; // no problem } // 同上 { let s1 = &s; let s2 = &s; println!("{}, {}", s1, s2); let s3 = &mut s; }

悬垂引用

悬垂指针:指针指向的内存可能已经被分配给其它持有者

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针

在 Rust 中,编译器确保引用永远也不会变成悬垂状态

例如如下悬垂引用会报错:

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

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,Rust 不允许这么做

引用的规则

  • 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
  • 引用必须总是有效的

Slice 类型

slice 允许引用合集中一段连续的元素序列,而不用引用整个集合

slice 是一类引用,所以没有所有权

本文作者:Morales

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 License 许可协议。转载请注明出处!