通俗易懂解释Rust所有权和借用概念

22-05-30 banq

Rust有三个主要概念:
  • 所有权(在同一时间只有一个变量 "拥有 "数据,并且所有者负责取消分配)
  • 借用(你可以向拥有的变量借用一个引用)
  • 生命周期(所有的数据都会跟踪它将被销毁的时间)

这些都是相当简单的概念,但它们往往与其他语言的概念背道而驰,所以我想试着更详细地解释一下这些概念。因为Rust并不是唯一使用这些概念的语言(例如,你可以在C++中做unique_ptr),学习这些概念不仅可以帮助你写出更好的Rust代码,而且可以写出更好的代码。

所有权
所有权的概念是,如果你拥有一个项目,你就负责在你完成后销毁这个项目。

在Rust中,一块数据在任何时候都只能有一个所有者。这与垃圾收集语言或带有原始指针的语言有很大的不同,因为你经常要 "玩弄 "数据,而不是对同一数据有多个引用,所以在同一时间只有一个变量拥有该数据。

拥有的数据只有在拥有的变量不再拥有该数据时才会被自动删除。这可能发生在以下情况。

  • 所有者变量超出范围并被销毁
  • 所有者变量被设置为另一个值,使原来的数据不再可被访问

这就简化了内存管理问题,并消除了在C或旧C++中经常发生的 "最后该由哪个指针来删除数据 "的混乱问题。所有权不是Rust独有的。现代C++推荐使用'unique_ptr',一个智能指针,它也 "拥有 "它所包裹的数据,而不是原始指针。

当你在Rust中声明一个变量时,该变量 "拥有 "数据。

let a = Box::new(2); // a "owns" a heap allocated integer


当一个变量拥有某样东西时,它可以使用赋值将其转移到其他变量。在让出其数据后,旧变量不能再访问该值,而新变量是新的所有者。

let mufasa = Box::new("king"); // mufasa is the owner of "king"
let scar = mufasa; // the data "king" is moved from mufasa to scar

println!("{}", scar); // scar is now the owner of "king"
println!("{}", mufasa); // ERROR: mufasa can no longer be accessed


在创建所有者的范围结束时,数据被销毁。

{
    let a = Box::new(2); // a owns a heap allocated integer
} // a's data deallocated here 


技巧和窍门
1、 将数值传入函数会将数据 "移动 "到函数变量中。一旦发生这种情况,原来的变量就不能被访问。这似乎很有限制性,这就是为什么下一个话题要尝试解决这个问题。

fn hello(a: Box<i32>) {
    println("{:?}", a); // prints "2"
}

fn main() {
    let b = Box::new(2);
    hello(b); // moves b into hello's a parameter
    
    b; // ERROR: cannot access b after it gave its value to a
}


2、在使用Rust时,一个常见的问题是,当数据被一个容器(如Vec或Option)所包围时,如果你想把数据取出来,你必须先手动克隆数据或把它从容器中移除。一个问题是,有时你想把数据从容器中移出,而又不妨碍对容器变量的访问。要做到这一点,你可以使用mem::replace函数,它将变量 "重置 "为某个值并返回拥有的数据。之后,原来拥有的变量不再拥有这些数据,而被设置为返回值的变量现在拥有这些数据。例如,这里是一个链接列表的代码片段。

use std::mem;

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    data: T,
    next: Link<T>,
}

pub struct Stack<T> {
    size: i32,
    head: Link<T>,
}

impl<T> Stack<T> {
    // ... other methods

    pub fn pop(&mut self) -> Option<T> {
        let head = mem::replace(&mut self.head, None); // retrieve the Node from the Option and and set self.head to be None
        head.map(|old_head| {
            let old_head = *old_head;
            self.head = old_head.next;
            self.size -= 1;
            
            old_head.data
        })
    }
}

因为这种情况在Options中特别常见,所以有一个Options的take()方法,做同样的事情,但不那么冗长。

impl<T> Stack<T> {
    // ... other methods
    
    pub fn pop(&mut self) -> Option<T> {
        self.head.take().map(|old_head| { // retrieve the Node from the Option and set self.head to be None
            let old_head = *old_head;
            self.head = old_head.next; // self.head is None so you can freely set it
            self.size -= 1;

            old_head.data
        })
    }
}


借用
上一节强调了所有权本身的一个大缺陷。在很多情况下,你想操作数据,但又不真正拥有数据。例如,你可能想把一个值传递给一个函数,但仍然能够在函数之外调用所有者变量。

Rust允许你使用借用的概念来做到这一点。借用就像你所想的那样,它只是允许另一个变量暂时借用你的变量中的数据,并在完成后将其归还。

Rust允许你有两种类型的借用。

  • 带有'&'的不可变的借用(你可以读取借用数据的值,但你不能修改它)
  • 带有'&mut'的可变借用(你可以读取和修改借用的数据的值)

你既可以有
  • 有很多不可变的借用
  • 只有一个可变的借用

在任何给定的时间内,在一个作用域中的一块数据只有一个可变的借用。所以你应该尽量在大多数时候做不可变的借用,只有在你真正需要的时候才做可变的借用。

要访问一个借用的引用中的值,你要使用解除引用操作符 "*"。

当你借用一个变量时,所有者变量变得不可访问,直到借用的变量被销毁。

let mut x = 5;
let y = &mut x; // y从x那里借用了数据。
println! ("{}", x); // ERROR: x不再拥有数据,y拥有它!

当一个借来的变量被销毁时,它会把借来的值还给所有者。

let mut x = 5;

{ 
    let y = &mut x; // y从x那里借来了数据。
    *y += 1; // y改变借用的数据
} // y把数据还给x

println!("{}", x); // x又拿回了数据(并且它被改为6)。

正因为如此,所有者要比借用者活得更久

let mut x: &i32;

{
    让y = 3;
    x = &y; // ERROR: x比y活得长!
} // y在这里被毁掉了! 如果Rust编译器不阻止这种情况的发生,x会发生什么?



技巧和窍门
函数参数最终大多是借来的引用,因为否则值会被移到函数内部。

函数的返回值不应该是对局部变量的引用,Rust不会让你这样做。如果你在C语言中返回了一个指向局部变量的指针,你可能会导致数据被破坏,或者返回值不知道什么时候要去掉它的数据,不管你怎么看,这都是不好的。

不要担心取消引用来 "读 "或 "写"(取决于&或&mut)一个借用的引用的值

enum State {
    Hello,
    Bye,
}
fn hello(blah: &State, foo: &mut i32) {
    match *blah { // you are only reading an immutable reference so its fine
        State::Hello => println!("Hello!"),
        State::Bye => println!("Bye!"),
    }
    
    *foo += 1; // you are only writing to a mutable reference so its fine
}


不要担心将解除引用的引用分配给变量,因为这将试图将数据转移到新的变量中。

enum State {
    Hello, 
    Bye,
}
fn hello(blah: &State) {
    let thief = *blah; // ERROR: blah can't give a borrowed item to thief! 
                       // 小偷要拿走值而不还给原主人
}

赋值并不是唯一会试图移出借来的引用的东西。例如,模式匹配也会试图移动值。为了防止这种情况,在匹配变量前面加上'ref'来借用匹配变量而不是移动

enum State {
    Hello(String),
    Bye,
}

fn hello(blah: &State) {
    match *blah {
        State::Hello(s) => println!("Hello, {}", s), // ERROR: moves data inside blah (the string) into the variable s
        State::Bye => println!("Bye!"),
    }
    
    // do this instead
    match *blah {
        State::Hello(ref s) => println!("Hello, {}", s), // borrow the string data inside blah
        State::Bye => println!("Bye!"),
    }
}

详细点击标题