2

Rust常见问题:所有权和可变性

 1 year ago
source link: https://www.jdon.com/62838
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Rust常见问题:所有权和可变性


使 Rust 成为如此出色的语言的相同概念可能会给新手带来问题:所有权、生命周期等。
并不是这些概念天生就很难;只是它们提供了其他语言没有的安全检查;这些安全检查可能会成为熟悉其他更宽容语言的人的障碍。

Rust 编译器严重依赖静态代码分析来查找内存错误。而其他语言(如 C 或 Java)允许开发人员编写可能导致不希望的状态(例如NullPointerException堆栈溢出)的代码,然后需要在运行时通过错误处理和异常来处理,Rust 不允许这样的代码编译。

在这篇文章中,我们将专注于初学者在开始他们的 Rust 世界之旅时会发现的常见问题。

dc1a1372cbbc4bd0865178e48eb72618~tplv-obj:1194:696.image?from=post&x-expires=1673193600&x-signature=midXCBV58Vzf9hQZVXS9Hg3maBM%3D

试图修改不可变的变量
在下面的例子中,我们创建了一个名为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那样的垃圾收集器,也不要求开发者手动 "销毁 "变量引用以释放分配的内存。相反,它使用所有权来定义何时清除变量引用的内存。当执行到一个函数的末端时,该函数所拥有的所有变量将超出范围,其内存将被释放。

  1. 当程序开始时,主函数拥有book变量的所有权。
  2. 然后,当我们调用print_book(book)时,函数print_book现在拥有book。
  3. 当执行到print_book的结束时,book将超出范围,其数据被清除。

这方面有多种解决方案:

  1. 改变print_book,把书book的所有权还给main。
  2. 将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

通过这个例子,我们展示了两点:

  1. Rc::clone(&book.title)返回对book.title引用的克隆,而不是对book.title值的克隆。否则,println! ("{}", title.borrow())会打印出旧的值 "The Rust Programming Language"。
  2. 为book.title使用Rc<RefCell<String>>变量类型,允许我们创建其引用的副本,并改变其值。

关于Rc和RefCell的警告
虽然Rc和RefCell允许我们创建一个更复杂的工作流程,但它们也增加了你的应用程序的复杂性。仅仅从人机工程学的角度来看,用<Rc<RefCell<T>>来传递变量很快就会变得非常冗长。

另外,在不需要的时候使用引用计数器会增加内存泄漏的风险。如果我们把一个引用副本保留太久,它所指向的底层内存可能不会被释放。在我们使用Rust实现HTTP服务器等东西的情况下,我们可能会积累不使用的对象的引用副本,增加消耗的内存量。这在Java等语言中是一个常见的问题,保留对未使用的变量的引用也会导致内存泄漏,而垃圾收集器无法修复。

在可能的情况下,我们必须避免过度使用这些工具,并尽量按照所有权的基本规则来工作。

结论
Rust的所有权和可变性规则与其他语言执行内存安全的方式完全不同。虽然这些工具会让新人在学习过程中感到困难,但它们也是Rust如此安全和轻量级的原因。

不仅所有权允许Rust在没有垃圾收集器等工具的情况下运行,而且它还迫使开发人员更深入地思考在他们的应用程序中如何分配、使用和释放内存。像C和C++这样的语言也提供了这种程度的控制,但却没有Rust的编译器所执行的保障措施。

虽然看起来Rust编译器抛出了太多的错误,但这是为了我们自己好。在编译过程中抓住这些问题比在执行时试图调试它们要好得多,在生产环境中很多时候都是如此。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK