本文的参考代码在这里
所有权(Ownership)
Rust 的所有权(Ownership)是一个核心概念,它帮助 Rust 在没有垃圾回收机制的情况下进行内存管理。在 Rust 中,每个值都有一个与之关联的变量,这个变量被称为该值的所有者(owner)。
以下是关于 Rust 所有权的一些关键规则:
- 一个值在任何时刻只能有一个所有者。
- 当值的所有者超出其作用域时,该值将被销毁。
这意味着 Rust 在运行时内存管理方面与许多其他编程语言不同。在某些语言中(如 C),开发人员需要显式地分配(allocate)和回收(deallocate)内存。而在其他具有垃圾回收功能的语言(如 Java)中,垃圾回收器会在程序运行时不断寻找不再使用的内存。但 Rust 的所有权系统允许它无需这些额外的机制就能保证内存安全。
- 垃圾回收器:在其他一些具有垃圾回收机制的语言中,由于垃圾回收器自动管理内存,可能会导致程序运行速度更快但性能表现更加不可预测。此外,大型程序会更大,手动管理内存可能会增加错误处理和管理的成本。然而,Rust 没有使用垃圾回收器,因此不会出现这些问题。
- 内存控制:使用 Rust 可以更好地控制内存的使用,从而避免内存泄漏或内存溢出等问题。此外,由于没有垃圾回收器,写入时间会更快,但大型程序的大小可能会导致更高的运行时延迟。
- 所有权模型:Rust 拥有所有权模型,这有助于减少内存错误并提高代码的可靠性。然而,这也会导致学习曲线变得更加陡峭,因为需要更多地了解内存管理和内存分配策略。
- 内存管理:Rust 具有更好的内存管理能力,这有助于优化性能。然而,这也会增加错误处理和维护成本。
- 学习曲线:由于 Rust 的内存管理和内存控制的复杂性,需要更多的学习和练习才能掌握它们。
所有权代码示例:
1 | fn main() { |
移动(Move)操作的数据交互
Rust 的移动(Move)概念,类似别的语言里的复制(copy),其原理在引用值复制上有着本质区别,这也是 Rust 语言独特的地方。
原始值复制
这种和其他语言的 copy 区别不大
1 | // 原始值的复制 |
引用值复制
先看一段 rust 代码:
1 | let s1 = String::from("hello"); |
如果按照其他语言的 copy 操作
- s1 被创建后,其指针、长度和容量存在于栈(Stack)中,其值”hello”存在于堆(Heap)中。
- s2 复制 s1,s2 创造出一块新的栈(Stack),存储其指针、长度和容量等属性,但共用堆中的”hello” 值。
- rust 对以上的操作做了优化,正如前面所说的,当 s2 被绑定了 s1 的堆(Heap)引用值后,s1 已超出范围(scope),rust 认为 s1 的值不再有效,直接丢弃(drop)了,示例图:
1 | let s1 = String::from("hello"); |
以上的 print 会报错如下:因为 s1 的值已经移动(Move)到 s2
1 | cargo run |
按照以上的报错,如果还需要用到s1
, rust 也保留了 copy 操作,我们必须用.clone()
进行拷贝,代码修改如下:
1 | let s1 = String::from("hello"); |
Move 在函数中的应用
1 | fn main() { |
上述代码中,
- 因 s3 值已经 move 到函数
takes_ownership(s3);
里,其值已经不存在 s3 上,而跳到了函数里 - 函数里的
some_str
传进来后,被限制在函数的作用域里后被销毁(drop) - 所以
println!("s3 {}", s3);
则会报错,因为s3
的值已被销毁在函数takes_ownership
的作用域里
而原始值则没有这个问题:
1 | fn main() { |
回到引用值的移动(move)的问题,如果要避免 move 后带来的影响,则须将引用值直接放函数里作为返回值即可,如:
1 | fn main() { |
或如下,变量 ss2
传入函数 takes_and_gives_back
后,仍要使用 ss2
,则须再写一个函数 takes_and_gives_back
将其值困
1 | fn main() { |
&
引用及借用 (References and Borrowing)
rust 允许使用 返回元组的解构
例如以下例子,x1
传入了函数 calculate_str_length(x1)
里,已经被移动了,
在外面如果要再获取 x1,则需返回其原值并将其解构出来,但这样返回值就复杂了,传入的同时还需返回他
1 | fn main() { |
如果传进的函数的值 x1
要在外面获取,但又不需要将其传回作为返回值,就需要用到引用:
1 | fn main() { |
上面的代码中,调用 str1 时,加入了 &
符号,即引用
- 函数里的引用变量会自行寻找值,而不是获取他的所有权,从而让进入函数的变量,在后续能被继续使用。
- 同样的,借用的变量
s
则不能进行其他操作,因为仅仅是借用。
可变引用(Mutable References)
上面代码解决的办法就是将 str1
定义为可迭代操作 mut
,以下是修改后的代码:
1 | fn main() { |
而引用有严格的限制, 例如以下代码:
1 | let mut str2 = String::from("kkkkkkkk"); |
上面定义的 r3
会报错,是因为:
r3
前定义的r1
和r2
已定义为非mut
变量,他们还未被使用- 要解决以上报错,则须将其移动到 print 之后即可,因
r1
,r2
定义后已被使用过
修改后:
1 | let mut str2 = String::from("kkkkkkkk"); |
悬空引用(Dangling References)
在函数内部,引用值不能作为返回值!这是因为引用值 s
已超出范围, s
值已被丢弃,以下是错误 ❌ 代码示范:
1 | fn dangle() -> &String { // dangle returns a reference to a String |
字符串切片类型(The Slice Type)
Rust 的切片是一种比较特殊的形式,这里我们单独讨论。以下是官方文档对切片所有权的总结:
Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice is a kind of reference, so it does not have ownership.
切片允许您引用集合中连续的元素序列,而不是整个集合。切片是一种引用,因此它没有所有权。
让我们看一个切片的例子,
1 | let str3: String = String::from("hello world"); |
上面的而切片有着一个特殊的类型 &str
, 同样的,定义字符串时,也可将其定义为 &str
,就像下面的例子:
1 | let str4: &str = "hello rust"; |
封装寻找第一个词的函数:
1 | fn main() { |
可以看出,我们传入和返回的类型均为 &str