[Rust入门系列1]所有权、引用、借用与Slice切片

[Rust入门系列1]所有权、引用、借用与Slice切片

所有权(Ownership)是Rust最为与众不同的特性。Rust并没有垃圾回收(Garbage collector)机制,凭借所有权系统即可保障内存安全。

栈(Stack)与堆(Heap)

中的所有数据都必须占用已知且固定的大小。

在编译时大小未知或大小可能变化的数据,要改为存储在上。

内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。(将数据推入栈中并不被认为是分配)。

所有权规则

1. Rust 中的每一个值都有一个 所有者(owner)。

2. 值在任一时刻有且只有一个所有者。

3. 当所有者(变量)离开作用域,这个值将被丢弃。

变量作用域(Scope)

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

移动、克隆与拷贝

在Rust中,标量(Scalar)是占用固定内存大小的数据类型(整形、浮点型、布尔类型和字符类型),它们可以储存在中。而String类型了支持一个可变,可增长的文本片段,需要在上分配一块在编译时未知大小的内存来存放内容。

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

在上面这个例子中,字符串"hello"的储存分为两部分:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在上。上存放内容的内存部分。
img

如图,这个字符串在内存中的储存分为两部分,左边是在栈里的部分,右边是在堆里的部分。

移动

我们先回顾一下在其他语言(比如Python)中,有浅拷贝深拷贝的概念。

a = ["a","b","c"]
b = a
print(f"a: {a}\nb: {b}")
a: ['a', 'b', 'c']
b: ['a', 'b', 'c']
b.append("d")
print(f"a: {a}\nb: {b}")
a: ['a', 'b', 'c', 'd']
b: ['a', 'b', 'c', 'd']

如上,是Python中浅拷贝的一个例子。将a直接复制给b时,只是将栈中的内容进行了复制,两个变量ab共同指向了同一个列表。
img

同时,Python中还可以实现深拷贝。

from copy import deepcopy
a = ["a","b"]
b = deepcopy(a)
print(f"a: {a}\nb: {b}")
a: ['a', 'b']
b: ['a', 'b']
b.append("c")
print(f"a: {a}\nb: {b}")
a: ['a', 'b']
b: ['a', 'b', 'c']

这样在内存中表现如下:
img

让我们试试在Rust进行“浅拷贝”会发生什么:

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

运行时会发生报错,禁止使用无效的引用

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:15
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{s1}, world!");
  |               ^^^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是叫做浅拷贝。上面的例子可以解读为 s1 被移动到了 s2 中。那么具体发生了什么,如图所示。
img
但注意,对于Copytrait的数据类型,移动(move)时会进行拷贝(copy)。

克隆

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

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

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

这样相当于Python中的深拷贝,最终输出结果为:s1 = hello, world!, s2 = hello, world!

拷贝

拷贝(copy)是一种trait,拥有Copytrait的数据在进行移动(move)时,还会隐式复制一份数据,使得旧变量不会失效。
拷贝操作会直接复制栈上的字节。

  let x = 5;
  let y = x;

  println!("x = {x}, y = {y}");

不同于 String 类型,整形等存储在栈上的类型拥有 Copy 的trait。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。

因此这段代码可以正常运行得出结果 x = 5, y = 5

同样,这里也可以正常调用clone。因为像整型这样的在编译时已知大小的类型被整个存储在栈上,这里没有深浅拷贝的区别。

Rust不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。

Drop trait 和 Copy trait

DropCopy是两个互斥的trait,不能同时存在于同一个类型。

什么样的类型(可以)拥有Copy Trait

  • 数据只存在栈上,不涉及堆内存或其他需要复杂管理的资源
  • 所有字段都实现了Copy
  • 浅拷贝(栈按位复制)是安全的
    Rust自带的标量带有Copy Trait:整形、布尔、浮点数、字符类型(char)

什么样的类型(可以)拥有Drop Trait

需要管理资源或自定义析构(销毁)逻辑的类型会实现Droptrait。
比如String类型。

为什么Copy和Drop Trait互斥

  • Copy表示类型可以通过简单的位复制安全创建副本,无需处理所有权或资源管理。
  • Drop表示类型需要管理资源,必须在析构时执行清理逻辑(如释放内存)。

拥有Copy trait的类型

作为一个通用的规则,任何一组简单标量值的组合都可以实现Copy,任何不需要分配内存或某种形式资源的类型都可以实现Copy 。如下是一些 Copy 的类型:

  • 所有整数类型
  • 布尔类型
  • 所有浮点数类型
  • 字符类型, char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如, (i32, i32) 实现了 Copy ,但 (i32, String) 就没有。
类型 方法 效果 备注
Copy trait 数据类型 copy (本质是move时同时隐式进行) 栈上数据复制 Drop trait数据类型不能进行copy,只能进行move或clone
Copy trait 数据类型 clone 栈与堆数据均复制 不在堆上分配内存的数据类型才一般能设置Copy trait,因此和copy效果相同
Drop trait 数据类型 clone 栈与堆数据均复制 在栈和堆均复制一份
Drop trait 数据类型 move 栈上数据复制并转移所有权,指向的堆位置不变 前一个变量失效

所有权与函数

上节提到,拥有Copytrait的数据类型(一般是只储存在栈上的)和拥有Droptrait的数据类型(需要堆内存或者其他复杂资源管理,同时析构时必须执行清理逻辑)都能进行移动(move)。只不过前者会隐式进行栈内存按位复制的操作,相当于其他语言的“浅拷贝”。后者在复制栈上内存的同时,会使前一个变量失效,所有权转移到新变量上。前者完成移动后,会有两个值分别对应两个所有者(Owner)。后者完成移动后,旧变量不再拥有任何值且被销毁,新变量接手值的所有权。

将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() {
    let s1 = String::from("halo");
    takeover(s1);
    // println!("{s1}");
}
fn takeover(one_string: String){
    println!("takeover has {one_string}");
}

会输出takeover has halo,但如果我们更改代码为

fn main() {
    let s1 = String::from("halo");
    takeover(s1);
    println!("{s1}");
}
fn takeover(one_string: String){
    println!("takeover has {one_string}");
}

则会编译时报错:

error[E0382]: borrow of moved value: `s1`
 --> main.rs:4:15
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     takeover(s1);
  |              -- value moved here
4 |     println!("{s1}")
  |               ^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `takeover` to borrow instead if owning the value isn't necessary
 --> main.rs:6:25
  |
6 | fn takeover(one_string: String){
  |    --------             ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     takeover(s1.clone());
  |                ++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

因为"hello"进入takeover()后,所有权从s1转手到函数takeover()。此时s1已失效,因此无法打印s1

如果我们再次更改代码

fn main() {
    let s1 = 2025i32;
    takeover(s1);
    println!("main has {s1}")
}
fn takeover(one_int: i32){
    println!("takeover has {one_int}");
}

会正常编译并输出

takeover has 2025
main has 2025

返回值与作用域

返回值也可以转移所有权。

fn main() {
    let s1 = String::from("hello");
    let s2 = takeover_giveback(s1);
    println!("main has \"{s2}\"");
    // println!("main has {s1}");
}
fn takeover_giveback(one_string: String) -> String {
    println!("takeover has \"{one_string}\" and give it back to main()");
    one_string
}

会得到

takeover has "hello" and give it back to main()
main has "hello"

"hello"的所有权交给了takeover_giveback()的形参,然后在这个函数中将值的所有权又返回了交给了s2。变量s1takeover_giveback(s1);时便会销毁(因为移动),takeover_giveback()的参数one_string在返回时销毁,s2在主函数结束时销毁,因为到了作用域结束。

但是s1依然是失效的:

fn main() {
    let s1 = String::from("hello");
    let s2 = takeover_giveback(s1);
    println!("main has \"{s2}\"");
    println!("main has {s1}");
}
fn takeover_giveback(one_string: String) -> String {
    println!("takeover has \"{one_string}\" and give it back to main()");
    one_string
}

会编译报错:

error[E0382]: borrow of moved value: `s1`
 --> main.rs:5:24
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = takeover_giveback(s1);
  |                                -- value moved here
4 |     println!("main has \"{s2}\"");
5 |     println!("main has {s1}");
  |                        ^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `takeover_giveback` to borrow instead if owning the value isn't necessary
 --> main.rs:7:34
  |
7 | fn takeover_giveback(one_string: String) -> String {
  |    -----------------             ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = takeover_giveback(s1.clone());
  |                                  ++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值会被清理掉(Copytrait型数据会直接出栈删掉,Droptrait型数据会自动运行定义的析构方式),除非数据被移动为另一个变量所有。

fn main() {
    let s1 = 114514i32;
    let s2 = takeover_giveback(s1);
    println!("main has {s2}");
    println!("main has {s1}");
}
fn takeover_giveback(one_int: i32) -> i32 {
    println!("takeover has {one_int} and give it back to main()");
    one_int
}

会得到结果:

takeover has 114514 and give it back to main()
main has 114514
main has 114514

因为s1在移动(move)时执行了copy

引用与借用

引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。与指针不同,引用确保指向某个特定类型的有效值。
引用使用符号&,与使用&引用相反的操作是解引用(dereferencing),它使用解引用运算符,*。Rust编译器在某些上下文中会自动插入解引用操作,简化代码,比如使用.操作符调用方法,以及println里面也会自动解引用。

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

    let len = calculate_length(&s1); // 创建了一个指向s1的指针,并将其传递给calculate_length函数内

    println!("The length of '{s1}' is {len}."); // s1只是被借用了,没有发生所有权转移,因此可以正常输出
}

fn calculate_length(s: &String) -> usize {  // 形参类型使用的是 指向String类值的引用
    s.len()  // Rust会自动解引用
} 

img

从内存角度讲,s1在栈中保存了(指针、长度和容量),然后指针指向堆中的内容。然后创建s1的引用时,在栈上新压入一个s,s的值本身就是指向s1栈中位置的地址(指针)。

我们将创建一个引用的行为称为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完后,必须还回去。因为我们并不拥有它的所有权。

正如变量默认是不可变的,引用也(默认)不允许修改引用的值。

可变引用

可变引用(mutable reference)允许我们修改一个借用的值。

fn main() {
    let mut s = String::from("hello"); // 原变量需要是可变的

    change(&mut s); // 创建引用时需要是可变的
    println!("{s}")
}

fn change(some_string: &mut String) {  // 形参也是可变的引用
    some_string.push_str(", world"); // 通过引用对原变量进行了改变
}

输出结果为hello, world。这就非常清楚地表明,change函数将改变它所借用的值。

可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。即同一时间内,对同一数据要么存在多个不可变引用,要么存在一个可变引用,二者不可共存

  • 允许创建多个不可变引用,但会阻止可变引用的创建,直到它们不再被使用。

  • 可变引用具有独占性,存在时不允许其他任何引用(无论是可变还是不可变)。

  • 引用的作用域由最后一次使用的位置决定(非词法生命周期,NLL)。

  • 不可变引用的作用域结束后,可以创建可变引用,反之亦然。

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

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    println!("{r1} and {r2}");
    // 此位置之后 r1 和 r2 不再使用,非词法生命周期结束

    let r3 = &mut s; // 没问题
    println!("{r3}");
}

悬垂引用(Dangling References)

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。

相比之下,在Rust中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

总结:引用的规则

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。

  • 引用必须总是有效的。

Slice类型

slice允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一种引用,所以它没有所有权。

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

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

不同于整个String的引用,hello是一个部分String的引用,由一个额外的[0..5]部分指定。可以使用一个由中括号中的[starting_index..ending_index]指定的range创建一个slice,其中starting_indexslice的第一个位置,ending_index则是slice最后一个位置的后一个值。在其内部,slice的数据结构存储了slice的开始位置和长度,长度对应于ending_index减去starting_index的值。所以对于let world = &s[6..11];的情况,world将是一个包含指向s索引 6 的指针和长度值 5 的slice

img

对于 Rust 的..range 语法,如果想要从索引 0 开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:

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

let slice = &s[0..2];
let slice = &s[..2];

依此类推,如果slice包含String的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:

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

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

也可以同时舍弃这两个值来获取整个字符串的slice。所以如下亦是相同的:

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

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

由于Rust不允许可变引用和不可变引用同时存在,我们可以规避下列代码:

fn main() {
    let mut s = String::from("hello world"); // 可变String "hello world"

    let word = first_word(&s); // 创建不可变引用来获取第一个单词

    s.clear(); // 错误!因为不可变引用的NLL在下行println才结束,此时对不可变引用指向的原String进行clear会试图获取一个可变引用来进行操作,可变与不可变不能同时存在

    println!("the first word is: {word}");
}

fn first_word(s: &String) -> &str {  // 形参是String的不可变引用,通过逻辑获取第一个单词(实际仍然是不可变引用的slice)
    let bytes = s.as_bytes();

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

    &s[..]  // 返回slice,这个引用指向的main中的s
}

会发生编译错误:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> main.rs:6:5
  |
4 |     let word = first_word(&s);
  |                           -- immutable borrow occurs here //创建不可变引用
5 |
6 |     s.clear(); // 错误!
  |     ^^^^^^^^^ mutable borrow occurs here // 对s进行clear会创建可变引用
7 |
8 |     println!("the first word is: {word}");
  |                                  ------ immutable borrow later used here // 不可变引用的NLL在这里才结束

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

s.clear();调用的函数签名是pub fn clear(&mut self)

修改数据时要通过可变引用完成

Rust 的规则始终如一:修改数据必须通过可变引用(&mut)或可变变量(mut),具体取决于操作方式。如果通过函数或方法修改数据,则需要传递可变引用(&mut

Rust通过严格的借用规则确保:

  • 可变操作的唯一性(无数据竞争)。

  • 编译时的内存安全(无需运行时检查)。

这种设计强制开发者在修改数据时显式声明可变性,避免了潜在的并发或逻辑错误

总结

  • 变量和值发生移动(move)时,会使前一个变量失效。如果变量有Copytrait,则还会发生拷贝,使得前一个变量不会失效,在栈上按位复制。

  • Rust自带的标量(Scalar)有Copytrait,如整形、浮点型、布尔、char。String类型分栈和堆两部分储存,拥有Droptrait。

  • DropCopytrait互斥,前者用于复杂资源管理并定义析构逻辑,后者一般用于按栈储存的数据。

  • clone可以让数据无论栈和堆上部分都复制一份。

  • Rust的值都有唯一的一个所有者(Ownership),值在任一时刻有且只有一个所有者,拥有者离开作用域(Scope)时,值被丢弃。

  • 值被丢弃时,Copytrait直接出栈,Droptrait运行析构逻辑。

  • 函数和返回值和变量赋值一样可以移动值的所有权。

  • 我们可以创建引用(即"借用")来指向、访问属于其他变量的数据。引用(&)确保指向某个特定类型的有效值

  • 可以使用*来解引用,但Rust一般会自动解引用。

  • 可变引用和不可变引用不能同时存在。

  • 可以创建多个不可变引用,但只能存在一个可变引用。

  • 可变引用和不可变引用的作用范围由最后一次使用的位置决定(非词法生命周期NLL)。

  • 通过函数修改值,需要通过可变引用&mut

-> 运算符去哪里了?

在C/C++语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。

Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是Rust中少数几个拥有这种行为的地方。

它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut* 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:

p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust对方法接收者的隐式借用让所有权在实践中更友好。

本文整理自官方教程

posted @ 2025-04-09 11:20  NearlyHeadlessJack  阅读(202)  评论(0)    收藏  举报