使 Rust 成为如此出色的语言的相同概念可能会给新手带来问题:所有权、生命周期等。
并不是这些概念天生就很难;只是它们提供了其他语言没有的安全检查;这些安全检查可能会成为熟悉其他更宽容语言的人的障碍。
Rust 编译器严重依赖静态代码分析来查找内存错误。而其他语言(如 C 或 Java)允许开发人员编写可能导致不希望的状态(例如NullPointerException堆栈溢出)的代码,然后需要在运行时通过错误处理和异常来处理,Rust 不允许这样的代码编译。
在这篇文章中,我们将专注于初学者在开始他们的 Rust 世界之旅时会发现的常见问题。

试图修改不可变的变量
在下面的例子中,我们创建了一个名为Book的结构实例,并试图为其标题title属性分配不同的值。
struct Book { title: String } fn main() { let book = Book { title: String::from("The Rust Programming Language") }; book.title = String::from("Another book") }
|
如果我们试图编译这段代码,我们将得到以下错误。
error[E0594]: cannot assign to `book.title`, as `book` is not declared as mutable
|
解决方案
这第一个问题很容易发现。默认情况下,所有的Rust变量都被声明为不可变的(只读/恒定)。这一决定的目的是为了让开发者在期望变量改变数值时能够明确。
如果我们想让book变得可变,我们必须将其声明为let mut book。
struct Book { title: String } fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; book.title = String::from("Another book") }
|
在下面的例子中可以发现这种相同错误的另一种味道:
let x = 1; x = 2; | error[E0384]: cannot assign twice to immutable variable `x
|
同样的解决方案也适用。使用let mut x = 1来允许未来对x进行分配。
试图使用一个 "已被移动 moved"的变量
按照前面的例子,我们现在定义两个函数,接收一个Book的实例并尝试对它做一些事情(例如打印它的标题)。
fn print_book(book: Book) { println!("Book: {}", book.title); } fn do_something_else(book: Book) { println!("Again: {}", book.title); } fn main() { let book = Book { title: String::from("The Rust Programming Language") }; print_book(book); do_something_else(book); }
|
编译器错误:error[E0382]: use of moved value: `book` --> src/main.rs:19:23 | 14 | let book = Book { | ---- move occurs because `book` has type `Book`, | which does not implement the `Copy` trait ... 18 | print_book(book); | ---- value moved here 19 | do_something_else(book); | ^^^^ value used here after move
|
解决方案
这个问题是由Rust围绕数据引用的所有权规则造成的。
Rust没有像Java那样的垃圾收集器,也不要求开发者手动 "销毁 "变量引用以释放分配的内存。相反,它使用所有权来定义何时清除变量引用的内存。当执行到一个函数的末端时,该函数所拥有的所有变量将超出范围,其内存将被释放。
- 当程序开始时,主函数拥有book变量的所有权。
- 然后,当我们调用print_book(book)时,函数print_book现在拥有book。
- 当执行到print_book的结束时,book将超出范围,其数据被清除。
这方面有多种解决方案:
- 改变print_book,把书book的所有权还给main。
- 将print_book改为接收借来的book的引用。
1、把书book的所有权还给main
下面是第一种方式:
fn print_book(book: Book) -> Book { println!("Book: {}", book.title); book } fn do_something_else(book: Book) { println!("Again: {}", book.title); } fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; book = print_book(book); do_something_else(book); }
|
注意,我们改变了book的声明,使其成为可变的,所以我们可以重新分配其值。另外,我们还更新了print_book,使其在完成后返回Book。我不太喜欢这种方法,因为我发现它很啰嗦,而且如果print_book被期望返回其他东西的话,就很难处理了。
2、使用借来的引用
以下代码将正确编译:
fn print_book(book: &Book) { println!("Book: {}", book.title); } fn do_something_else(book: Book) { println!("Again: {}", book.title); } fn main() { let book = Book { title: String::from("The Rust Programming Language") }; print_book(&book); do_something_else(book); }
|
注意我们更新了print_book来接收一个&Book的实例,它是对Book实例的一个不可变的引用。这就是所谓的借用。我们还将print_book(book)改为print_book(&book),以向函数发送一个book的引用。
现在,print_book不会得到book的所有权。它将收到只读的引用。当执行到print_book结束时,book不会超出范围,因为main仍然拥有它。
请记住,在这个例子中,do_something_else确实获得了book的所有权,这是完全合法的。
另一个解决方案是听从编译器的建议,为Book实现Copy属性,这样在传递给其他函数时就会创建book的副本。但这样我们就会使用不同的book实例,这有一些缺点:
- 额外的内存分配。如果book很大,而我们每次把它传给一个函数时都会复制它,我们就会不必要地消耗内存。
- 不能很好地发挥变化的作用。如果其中一个函数要改变book的内容,那么我们必须返回副本,以便下一个函数可以访问book的更新值。这导致了与返回所有权相同的流程,但有额外的步骤。
试图在同一作用域内借用为可变和不可变的
现在,我们将用一个新的函数来修改书的标题,并用另一个函数来打印它。下面的代码将正确编译。
fn rename_book(book: &mut Book) { book.title = String::from("Something else"); } fn print_title(book: &Book) { println!("Book: {}", book.title); } fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; rename_book(&mut book); print_title(&book); }
|
然而,你会发现,做下面的事情--似乎实际上是在做同样的事情--并不能编译。
fn rename_book(book: &mut Book) { book.title = String::from("Something else"); } fn print_title(book: &Book) { println!("Book: {}", book.title); } fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; let mut book1 = &mut book; let book2 = &book; rename_book(&mut book1); print_title(&book2); } error[E0502]: cannot borrow `book` as immutable because it is also borrowed as mutable --> src/main.rs:20:17 | 19 | let mut book1 = &mut book; | --------- mutable borrow occurs here 20 | let book2 = &book; | ^^^^^ immutable borrow occurs here 21 | 22 | rename_book(&mut book1); | ---------- mutable borrow later used here
|
解决方案
Rust不允许我们在同一作用域内创建一个对象的可变和不可变的引用。
在第一个例子中,实际的借用发生在执行到每个rename_book和print_title函数时,所以每次只有一个借用的引用。
虽然这个问题最明显的解决方案是使用第一个例子,在这个例子中一切都能正确编译,但让我们探索一个更有创意的解决方案,这将对所有权规则有一些启发。
fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; { let mut book1 = &mut book; rename_book(&mut book1); } let book2 = &book; print_title(&book2); }
|
这个小小的变通方法使我们的代码又可以编译了。
当我们用一个块{}包裹book1和rename_book的借入实例时,我们为它们创建了一个新的作用域。
一旦执行到该块的末尾,在该块内声明的任何变量都将超出作用域范围。那么,当执行到let book2的时候,book1就不存在了,我们就只有一个借用。
试图保留对一个结构值的引用
当我们从其他语言转过来,我们试图为一个对象的属性保留一个引用是很常见的。
fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; let title = book.title; rename_book(&mut book); println!("Title: {}", title); }
|
虽然这在其他语言中是正确的,但Rust会抱怨。
error[E0382]: borrow of partially moved value: `book` --> src/main.rs:20:17 | 18 | let title = book.title; | ---------- value partially moved here 19 | 20 | rename_book(&mut book); | ^^^^^^^^^ value borrowed here after partial move
|
解决办法
当我们做 let title = book.title 时,我们将标题title属性的所有权从 book 传递给了主函数,这意味着我们不能在 rename_book 中使用 book。我们可以将title的所有权交还给book,但这将使我们无法单独使用title变量。
为了解决这个问题,我们可以克隆标题,如let title = book.title.clone()。然而,这种方法只有在以下情况下才有效:第一,title实现了Clone特性(字符串默认是这样的);第二,无论我们用title做什么,都不希望得到book.title的实际值。
fn rename_book(book: &mut Book) { book.title = String::from("Something else"); } fn print_title(book: &Book) { println!("Title: {}", book.title); } fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; let title = book.title.clone(); rename_book(&mut book); println!("Title: {}", title); print_title(&book); } /////////// Title: The Rust Programming Language Title: Something else
|
如果我们想保留对book.title的真实引用,我们必须借用它的引用,注意不要在同一作用域范围内创建不可变和可变的借用。
fn main() { let mut book = Book { title: String::from("The Rust Programming Language") }; { rename_book(&mut book); } let title = &book.title; println!("Title: {}", title); print_title(&book); } ///////// Title: Something else Title: Something else
|
当然,这只是一个非常自足的例子,在其他情况下是行不通的。例如,如果我们想保留对book.title的引用--与book无关--并从一个函数中返回它,该怎么办?
fn create_book() -> (Book, &String) { let book = Book { title: String::from("The Rust Programming Language") }; return (book, &book.title); } fn main() { let (mut book, title) = create_book(); rename_book(&mut book); println!("{}", &title); }
|
错误:
error[E0106]: missing lifetime specifier --> src/main.rs:16:28 | 16 | fn create_book() -> (Book, &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
|
我们不会深入讨论生命周期寿命的话题,但Rust的意思是,由于我们要返回一个借来的对String的引用,所以开发者需要在返回类型声明中添加一个生命周期lifetime寿命,以让Rust知道这个引用预计能活多久。
我们可以尝试--剧透一下,不成功--听从Rust编译器的建议,用一个 "静态寿命static lifetime "来注释&String。
fn create_book() -> (Book, &'static String) { let book = Book { title: String::from("The Rust Programming Language") }; return (book, &book.title); } error[E0382]: borrow of moved value: `book` --> src/main.rs:21:19 | 17 | let book = Book { | ---- move occurs because `book` has type `Book`, which does not implement the `Copy` trait ... 21 | return (book, &book.title); | ---- ^^^^^^^^^^^ value borrowed here after move | | | value moved here
|
在create_book函数结束时,book被移出了函数,所有权被传递给了调用者(main());正因为如此,我们无法借用book.title(这与我们在本帖第一个例子中发现的问题相同)。
即使我们试图存储书book的title的借用引用,我们也不能直接返回book.title的借用引用。
fn create_book() -> (Book, &'static String) { let book = Book { title: String::from("The Rust Programming Language") }; let title = &book.title; return (book, title); } error[E0515]: cannot return value referencing local data `book.title` --> src/main.rs:23:12 | 21 | let title = &book.title; | ----------- `book.title` is borrowed here 22 | 23 | return (book, title); | ^^^^^^^^^^^^^ returns a value referencing data owned by the current function
|
即使看起来我们把title的所有权传给了main(),实际上,我们是想返回一个由create_book拥有的变量的引用;这样的变量也会在create_book结束时超出作用域范围。
我们在这里有什么选择?
使用Rc来创建不可变的引用
基本的所有权规则在较简单的应用程序中运行良好。然而,更大、更复杂的代码将不可避免地需要获得一个对象的多个引用,并在各函数间传递。对于这些情况,std::rc::Rc-引用计数器-是一个方便的工具。
引用计数器允许我们为一个对象创建多个不可变的引用。这些引用可以安全地从一个函数传递到另一个函数。
use std::rc::Rc; struct Book { title: Rc<String> } fn create_book() -> (Book, Rc<String>) { let book = Book { title: Rc::new(String::from("The Rust Programming Language")) }; let title = Rc::clone(&book.title); return (book, title); } fn main() { let (book, title) = create_book(); println!("{}", book.title); println!("{}", title); } //////////// The Rust Programming Language The Rust Programming Language
|
在引擎盖下,Rc对已经创建的引用的数量进行统计。一旦所有的引用超出了作用域范围,它们所指向的对象就会从内存中被删除。在某种程度上,Rc是一个非常简化的垃圾收集器。
这个例子有点误导,因为看起来我们是在克隆book.title的值,其实我们是在克隆它的一个引用。如果我们更新book.title的值,这个区别就更清楚了。
使用Rc和RefCell来创建可变的引用
默认情况下,引用计数是不可变的。如果我们想更新book.title,我们必须引入RefCell来使title成为可变的引用。
use std::rc::Rc; use std::cell::RefCell; struct Book { title: Rc<RefCell<String>> } fn create_book() -> (Book, Rc<RefCell<String>>) { let book = Book { title: Rc::new(RefCell::new(String::from("The Rust Programming Language"))) }; let title = Rc::clone(&book.title); return (book, title); } fn rename_book(book: &Book) { book.title.replace(String::from("Something else")); } fn main() { let (book, title) = create_book(); rename_book(&book); println!("{}", book.title.borrow()); println!("{}", title.borrow()); } 输出:
Something else Something else
|
通过这个例子,我们展示了两点:
- Rc::clone(&book.title)返回对book.title引用的克隆,而不是对book.title值的克隆。否则,println! ("{}", title.borrow())会打印出旧的值 "The Rust Programming Language"。
- 为book.title使用Rc<RefCell<String>>变量类型,允许我们创建其引用的副本,并改变其值。
关于Rc和RefCell的警告
虽然Rc和RefCell允许我们创建一个更复杂的工作流程,但它们也增加了你的应用程序的复杂性。仅仅从人机工程学的角度来看,用<Rc<RefCell<T>>来传递变量很快就会变得非常冗长。
另外,在不需要的时候使用引用计数器会增加内存泄漏的风险。如果我们把一个引用副本保留太久,它所指向的底层内存可能不会被释放。在我们使用Rust实现HTTP服务器等东西的情况下,我们可能会积累不使用的对象的引用副本,增加消耗的内存量。这在Java等语言中是一个常见的问题,保留对未使用的变量的引用也会导致内存泄漏,而垃圾收集器无法修复。
在可能的情况下,我们必须避免过度使用这些工具,并尽量按照所有权的基本规则来工作。
结论
Rust的所有权和可变性规则与其他语言执行内存安全的方式完全不同。虽然这些工具会让新人在学习过程中感到困难,但它们也是Rust如此安全和轻量级的原因。
不仅所有权允许Rust在没有垃圾收集器等工具的情况下运行,而且它还迫使开发人员更深入地思考在他们的应用程序中如何分配、使用和释放内存。像C和C++这样的语言也提供了这种程度的控制,但却没有Rust的编译器所执行的保障措施。
虽然看起来Rust编译器抛出了太多的错误,但这是为了我们自己好。在编译过程中抓住这些问题比在执行时试图调试它们要好得多,在生产环境中很多时候都是如此。