RUST所有权与借用

所有权规则

  • Rust 中的每个值都有一个所有者
  • 同一时间只能有一个所有者。
  • 当所有者超出范围时,该值将被删除。
  • 变量作用域 既然我们已经掌握了基本语法,将不会在之后的例子中包含 fn main() { 代码,所以如果你 是一路跟过来的,必须手动将之后例子的代码放入一个 main 函数中。这样,例子将显得更加 简明,使我们可以关注实际细节而不是样板代码。 在所有权的第一个例子中,我们看看一些变量的 作用域(scope)。作用域是一个项(item) 在程序中有效的范围。假设有这样一个变量:
let s = "hello";

 变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声 明的点开始直到当前 作用域 结束时都是有效的。示例 4-1 中的注释标明了变量 s 在何处是有 效的。

# fn main() {
 { // s 在这里无效,它尚未声明
 let s = "hello"; // 从此处起,s 是有效的
 // 使用 s
 } // 此作用域已结束,s 不再有效
# }
String 类型

这个类型管理被分配到堆上的数据,String 是一个用于存储动态字符串的类型

String&str 的区别

  • &str

    • 不可变字符串切片,常用于存储静态字符串字面量。
    • 长度在编译时已知,不支持动态扩展。
    • 通常以引用的方式使用,如 &str,并指向字符串数据的某个位置。
  • String

    • 可变字符串类型,可以在运行时动态扩展。
    • 存储在堆上,大小不固定。
    • 提供了丰富的方法来操作字符串数据。、

创建 String

fn main() {
    let s1 = String::new(); // 创建一个空的 String
    let s2 = String::from("Hello, world!"); // 从字面量创建 String
    let s3 = "Hello, world!".to_string(); // 通过 to_string 方法创建 String
}

操作 String

fn main() {
    let mut s = String::from("Hello");
    s.push_str(", world!"); // 添加字符串切片
    s.push('!'); // 添加字符
    println!("{}", s); // 输出:Hello, world!!
}

内存与分配

就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文 件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。 不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并 且它的大小还可能随着程序运行而改变。 对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未 知大小的内存来存放内容。这意味着: • 必须在运行时向内存分配器(memory allocator)请求内存。 • 需要一个当我们处理完 String 时将内存返回给分配器的方法。 第一部分由我们完成:当调用 String::from 时,它的实现 (implementation) 请求其所需的内 存。这在编程语言中是非常通用的。 然而,第二部分实现起来就各有区别了。在有 垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。在大部分没有 GC 的语言中,识别 出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的 角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过 早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确的为一个 allocate 配对一个 free 。

现在看看这个 String 版本:

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

String的内部结构

你提供的图片展示了String的内部结构,如下:

  • ptr: 这是一个指向堆内存中实际存储字符串数据的指针。
  • len: 这是字符串的长度,即当前字符串中字符的个数。
  • capacity: 这是分配给字符串的堆内存的总容量,可以容纳的最大字符数。

长度表示 String 的内容当前使用了多少字节的内存。容量是 String 从分配器总共获取了多 少字节的内存。长度与容量的区别是很重要的,不过在当前上下文中并不重要,所以现在可以 忽略容量。 当我们将 s1 赋值给 s2 ,String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、 长度和容量。我们并没有复制指针指向的堆上数据。换句话说,内存中数据的表现如图 4-2 所 示。

图 4-2:变量 s2 的内存表现,它有一份 s1 指针、长度和容量的拷贝 这个表现形式看起来 并不像 图 4-3 中的那样,如果 Rust 也拷贝了堆上的数据,那么内存看起 来就是这样的。如果 Rust 这么做了,那么操作 s2 = s1 在堆上数据比较大的时候会对运行时 性能造成非常大的影响。

图 4-3:另一个 s2 = s1 时可能的内存表现,如果 Rust 同时也拷贝了堆上的数据的话 之前我们提到过当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存。不过 图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 s2 和 s1 离开作用域, 它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提 到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的 安全漏洞。

为了确保内存安全,在 let s2 = s1; 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。看看在 s2 被创建之后尝试使用 s1 会发生什么;这段代码 不能运行:

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

如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指 针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了, 这个操作被称为 移动(move),而不是叫做浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。那么具体发生了什么,如图 4-4 所示。

图 4-4:s1 无效之后的内存表现 这样就解决了我们的问题!因为只有 s2 是有效的,当其离开作用域,它就释放自己的内存, 完毕。 另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制都可以被认为是对运行时性能影响较小的。 变量与数据交互的方式(二):克隆 如果我们 确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个 叫做 clone 的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功 能,所以之前你可能已经见过了。 这是一个实际使用 clone 方法的例子:

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

这段代码能正常运行,并且明确产生图 4-3 中行为,这里堆上的数据 确实 被复制了。 当出现 clone 调用时,你知道一些特定的代码被执行而且这些代码可能相当消耗资源。你很 容易察觉到一些不寻常的事情正在发生。 只在栈上的数据:拷贝 这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,它们是示例 4-2 中的一 部分:

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

但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone ,不过 x 依然有效且没有被 移动到 y 中。

原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速 的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所 以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第 十章将会详细讲解 trait)。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给 其他变量后仍然可用。 Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离 开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。要学习如何为你 的类型添加 Copy 注解以实现该 trait,请阅读附录 C 中的 “可派生的 trait”。

那么哪些类型实现了 Copy trait 呢?你可以查看给定类型的文档来确认,不过作为一个通用的 规则,任何一组简单标量值的组合都可以实现 Copy ,任何不需要分配内存或某种形式资源的 类型都可以实现 Copy 。如下是一些 Copy 的类型:

• 所有整数类型,比如 u32 。

• 布尔类型,bool ,它的值是 true 和 false 。

• 所有浮点数类型,比如 f64 。 • 字符类型,char 。

• 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy ,但 (i32, String) 就没有。

所有权与函数 将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句 一样。示例 4-3 使用注释展示变量何时进入和离开作用域:

fn main() {
 let s = String::from("hello"); // s 进入作用域
 takes_ownership(s); // s 的值移动到函数里 ...
 // ... 所以到这里不再有效
 let x = 5; // x 进入作用域
 makes_copy(x); // x 应该移动函数里,
 // 但 i32 是 Copy 的,
 // 所以在后面可继续使用 x
} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
 // 没有特殊之处
fn takes_ownership(some_string: String) { // some_string 进入作用域
 println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
 // 占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
 println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处

示例 4-3:带有所有权和作用域注释的函数 当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。这些静态检查使 我们免于犯错。

试试在 main 函数中添加使用 s 和 x 的代码来看看哪里能使用它们,以及所 有权规则会在哪里阻止我们这么做。 返回值与作用域 返回值也可以转移所有权。示例 4-4 展示了一个返回了某些值的示例,与示例 4-3 一样带有类 似的注释。 文件名:src/main.rs

fn main() {
 let s1 = gives_ownership(); // gives_ownership 将返回值
 // 转移给 s1
 let s2 = String::from("hello"); // s2 进入作用域
 let s3 = takes_and_gives_back(s2); // s2 被移动到
 // takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
 // 所以什么也不会发生。s1 离开作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 会将
 // 返回值移动给
// 调用它的函数
 let some_string = String::from("yours"); // some_string 进入作用域。
 some_string // 返回 some_string
 // 并移出给调用的函数
//
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
 //
 a_string // 返回 a_string 并移出给调用的函数
}

示例 4-4: 转移返回值的所有权 变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量 离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。 虽然这样是可以的,但是在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们 想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进 去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。 我们可以使用元组来返回多个值,如示例 4-5 所示。 文件名:src/main.rs

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)
}

示例 4-5: 返回参数的所有权 但是这未免有些形式主义,而且这种场景应该很常见。幸运的是,Rust 对此提供了一个不用 获取所有权就可以使用值的功能,叫做 引用(references)。

借用与引用

示例 4-5 中的元组代码有这样一个问题:我们必须将 String 返回给调用函数,以便在调用 calculate_length 后仍能使用 String ,因为 String 被移动到了 calculate_length 内。相 反我们可以提供一个 String 值的引用(reference)引用(reference)像一个指针,因为它 是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确 保指向某个特定类型的有效值。

下面是如何定义并使用一个(新的)calculate_length 函数,它以一个对象的引用作为参数 而不是获取值的所有权: 文件名:src/main.rs

fn main() {
 let s1 = String::from("hello");
 let len = calculate_length(&s1);
 println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
 s.len()
}

首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1 给 calculate_length ,同时在函数定义中,我们获取 &String 而不是 String 。这些 & 符号就 是 引用,它们允许你使用值但不获取其所有权。图 4-5 展示了一张示意图。

注意:与使用 & 引用相反的操作是 解引用(dereferencing),它使用解引用运算符, * 。我们将会在第八章遇到一些解引用运算符,并在第十五章详细讨论解引用。

看这个函数调用:

# fn main() {
 let s1 = String::from("hello");
 let len = calculate_length(&s1);
#
# println!("The length of '{s1}' is {len}.");
# }
#
# fn calculate_length(s: &String) -> usize {
# s.len()
# }

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

# fn main() {
# let s1 = String::from("hello");
#
# let len = calculate_length(&s1);
#
# println!("The length of '{}' is {}.", s1, len);
# }
#
fn calculate_length(s: &String) -> usize { // s 是 String 的引用
 s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
 // 所以什么也不会发生

变量 s 有效的作用域与函数参数的作用域一样,不过当 s 停止使用时并不丢弃引用指向的数 据,因为 s 并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有 权,因为就不曾拥有所有权。

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

尝试示例 4-6 中的代码。剧透:这行不通! 文件名:src/main.rs

fn main() {
 let s = String::from("hello");
 change(&s);
}
fn change(some_string: &String) {
 some_string.push_str(", world");
}

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

可变引用 我们通过一个小调整就能修复示例 4-6 代码中的错误,允许我们修改一个借用的值,这就是 可变引用(mutable reference): 文件名:src/main.rs

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

首先,我们必须将 s 改为 mut 。然后在调用 change 函数的地方创建一个可变引用 &mut s , 并更新函数签名以接受一个可变引用 some_string: &mut String 。

这就非常清楚地表明, change 函数将改变它所借用的值。 可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的 引用。这些尝试创建两个 s 的可变引用的代码会失败: 文件名:src/main.rs

# fn main() {
 let mut s = String::from("hello");
 let r1 = &mut s;
 let r2 = &mut s;
 println!("{}, {}", r1, r2);
# }

错误如下:

$ cargo run
 Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
 |
4 | let r1 = &mut s;
 | ------ first mutable borrow occurs here
5 | let r2 = &mut s;
 | ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
 | -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

这个报错说这段代码是无效的,因为我们不能在同一时间多次将 s 作为可变变量借用。第一 个可变的借入在 r1 中,并且必须持续到在 println! 中使用它,但是在那个可变引用的创建 和它的使用之间,我们又尝试在 r2 中创建另一个可变引用,该引用借用与 r1 相同的数据。

这一限制以一种非常小心谨慎的方式允许可变性,防止同一时间对同一数据存在多个可变引 用。新 Rustacean 们经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。这 个限制的好处是 Rust 可以在编译时就避免数据竞争。

数据竞争(data race)类似于竞态条 件,它可由这三个行为造成: • 两个或更多指针同时访问同一数据。 • 至少有一个指针被用来写入数据。 • 没有同步数据访问的机制。 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情 况的发生,因为它甚至不会编译存在数据竞争的代码! 一如既往,可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同 时 拥有:

# fn main() {
 let mut s = String::from("hello");
 {
 let r1 = &mut s;
 } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
 let r2 = &mut s;
# }

Rust 在同时使用可变与不可变引用时也采用的类似的规则。这些代码会导致一个错误:

# fn main() {
 let mut s = String::from("hello");
 let r1 = &s; // 没问题
 let r2 = &s; // 没问题
 let r3 = &mut s; // 大问题
 println!("{}, {}, and {}", r1, r2, r3);
# }

错误如下:

$ cargo run
 Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
 --> src/main.rs:6:14
 |
4 | let r1 = &s; // no problem
 | -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
 | ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
 | -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

哇哦!我们 也 不能在拥有不可变引用的同时拥有可变引用。 不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是 可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。 注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次 使用不可变引用(println! ),发生在声明可变引用之前,所以如下代码是可以编译的:

# fn main() {
 let mut s = String::from("hello");
 let r1 = &s; // 没问题
 let r2 = &s; // 没问题
 println!("{} and {}", r1, r2);
 // 此位置之后 r1 和 r2 不再使用
88/599
Rust 程序设计语言 简体中文版
 let r3 = &mut s; // 没问题
 println!("{}", r3);
# }

不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器可以在作用域结束之前 判断不再使用的引用。 尽管这些错误有时使人沮丧,但请牢记这是 Rust 编译器在提前指出一个潜在的 bug(在编译 时而不是在运行时)并精准显示问题所在。这样你就不必去跟踪为何数据并不是你想象中的那 样。

悬垂引用(Dangling References) 在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针 (dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下, 在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保 数据不会在其引用之前离开作用域。 让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免: 文件名:src/main.rs

fn main() {
 let reference_to_nothing = dangle();
}
fn dangle() -> &String {
 let s = String::from("hello");
 &s
}

这里是错误:

$ cargo run
 Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
 |
5 | fn dangle() -> &String {
 | ^ expected named lifetime parameter
 |
 = help: this function's return type contains a borrowed value, but there is no
value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're
returning a borrowed value from a `const` or a `static`
 |
5 | fn dangle() -> &'static String {
 | +++++++
help: instead, you are more likely to want to return an owned value
 |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
 |
error[E0515]: cannot return reference to local variable `s`
 --> src/main.rs:8:5
 |
8 | &s
 | ^^ returns a reference to data owned by the current function
Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors

错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。第十章会详细介绍生命周 期。不过,如果你不理会生命周期部分,错误信息中确实包含了为什么这段代码有问题的关键 信息:

this function's return type contains a borrowed value, but there is no value for it to be borrowed from

让我们仔细看看我们的 dangle 代码的每一步到底发生了什么: 文件名:src/main.rs

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

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

# fn main() {
# let string = no_dangle();
# }
#
fn no_dangle() -> String {
 let s = String::from("hello");
 s
}

这样就没有任何错误了。所有权被移动出去,所以没有值被释放。

引用的规则 让我们概括一下之前对引用的讨论:

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

• 引用必须总是有效的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值