[Rust杂谈](1)Rust特性-所有权以及智能指针多所有权

1,348 阅读5分钟

前言

有点久没写文章了,最近正在学Rust于是打算就着Rust来粗略的进行一个关于Rust独有的所有权相关知识归纳。

所有权

Rust中每一个值都有且只有一个所有者变量,可以称作所有者变量有那个值的所有权,只有一个所有者变量保证了Rust可以通过该变量离开作用域时进行内存回收并管理,而非使用垃圾回收garbage collector,或者手动管理内存。

举个例子:

fn main() {
    // 创建了一个有所有权的变量
    let foo = String::from("Nctdt");
    // foo 的所有权转移给了 bar
    let bar = foo;
    println!("{}", foo); // error
    println!("{}", bar);
}

这个例子中,会报一个borrow of moved value: foo的错误,意思是foo变量的值的所有权已经被移走了,即移给了bar变量。

image.png

如图所示,原来的foo变量被废弃了,不可再用,因为内部的值的所有权已经被转移走了。

作用域

Rust是块级作用域,即一个代码块就有一个作用域,变量声明后的变量使用等都是在作用域内有效。比如有这么一个例子:

fn main() {
    { // 开启了个块级作用域
        let s = String::from("Nctdt");
        println!("{}", s);
    } // 此作用域结束,外部访问不到作用域内部的变量和值
    println!("{}", s); // error: not found in this scope
}

在该例子中,创建了个作用域内变量s,在该作用域外是访问不了作用域内的变量,但是内部作用域可以访问到外部作用域的变量且同时会将所有权转移进去。比如:

fn main() {
    let s = String::from("Nctdt");
    {
        s; // 将 s 转移进内部的作用域,即所有权被转移
    }
    println!("{}", s); // error: borrow of moved value: `s`
}

这个例子的变量s被转移进了内部作用域,因此在外部作用域中再也访问不了变量s了。

当数据离开作用域时

在作用域结束时,会自动清理内部的变量,以此达成内存清理和管理的目的,我们也可以实现在清理时需要做的操作,这个需要使用到Rust提供的Drop trait

该例子需要使用到structtrait但是在此不过多介绍。

struct Person {
    name: String,
}

impl Drop for Person {
    fn drop(&mut self) {
        println!("{}被清理了:)", self.name);
    }
}

fn main() {
    let s = Person {
        name: String::from("Nctdt"),
    };
    {
        s;
    } // 此时内部有所有权的变量为:`s` ,
      // Rust 会在结束作用域时调用内部有所有权的变量的Drop实现的drop方法,即`s`的drop方法。
    println!("s已经被清理了");
}

例子中的输出顺序为:

Nctdt被清理了:)
s已经被清理了

因为drop方法是在当前作用域结束时就会调用,因此Nctdt被清理了:)会在s已经被清理了之前。

借用

那总不能所有权总是转来转去的,那太麻烦了,因此可以使用借用(引用)。借用是可以在不移动所有权的情况下使用内部的值。let borrow_s = &s,即为借用了s

举个不使用借用的例子:

fn move_string(s: String) -> String {
    println!("{} 的所有权被移动进了该函数内", s);
    s
}

fn main() {
    let s = String::from("Nctdt");
    let s = move_string(s);
    println!("{}", s);
}

在该例子中,s的所有权被转移了两次,第一次是调用move_string(s)的时候s的所有权被转移进去了,第二次则是move_string返回s的所有权并将所有权转移给了外部的s,即let s = move_string(s)Rust允许同名变量的存在,之后的值会覆盖掉之前的值,虽然在这个例子中前面的值已经不能用了。

改为使用借用:

fn move_string(s: &String) {
    println!("使用了 {} 的借用", s);
}

fn main() {
    let s = String::from("Nctdt");
    move_string(&s);
    println!("{}", s);
}

在该例子,使用了&的语法进行借用,&String代表是String类型的借用,&s代表借用了s这个变量。于是,我们就解决了所有权到处转的问题了。

多所有权智能指针

那总在某些时候需要多所有权的情况,而借用又不适合,不过这种情况在这就不多做描述。这时候就需要使用多所有权智能指针,又称引用计数(reference counting)指针,类型为Rc<T>,通过use std::rc::Rc;

引用计数内部维护了一个指针和两个计数,指针用来指向源数据,计数用来记有多少个变量拥有源数据的所有权,计数分别为强引用计数和弱引用计数,当强引用计数在Drop后计数为0,则抛弃整个智能指针,举个例子:

use std::rc::Rc;

fn main() {
    let foo = Rc::new(1);
    let bar = Rc::clone(&foo);
    println!("foo: {}", foo);
    println!("bar: {}", bar);
    println!(
        "strong_count: {} {}",
        Rc::strong_count(&foo),
        Rc::strong_count(&bar)
    );
    let foo_weak = Rc::downgrade(&foo);
    println!("foo_weak: {}", foo_weak.upgrade().unwrap());
    println!("weak_count: {}", Rc::weak_count(&foo))
}

输出为:

foo: 1
bar: 1
strong_count: 2 2
foo_weak: 1      
weak_count: 1 

这个例子通过Rc::new(1)创建了第一个强引用变量foo,然后通过Rc::clone创建了第二个强引用变量barRc::clone指向的数据和传入的参数是同一个数据,此时的强引用计数则为2,接下来通过Rc::downgrade创建弱引用变量foo_weak,弱引用变量需要通过.upgrade()进行变量数据的使用,因为弱引用不关于数据是否被抛弃,只有强引用计数为0的时候数据才会被抛弃,所以.upgrade()返回的是一个Option枚举,因为可能在使用.upgrade()时数据已经被抛弃了,当被抛弃时返回的即是Option::None,此处因为保证数据存在,直接使用.upgrade().unwrap()进行数据获取即可,关系如图所示:

image.png

Rc内部实现远比此复杂,但是大体可以这么理解。当强/弱引用变量离开作用域时,会自动处理对应数据的关系,如:

use std::rc::Rc;

fn main() {
    let foo = Rc::new(1);
    {
        let bar = Rc::clone(&foo);
        println!("strong_count: {}", Rc::strong_count(&foo));
        let foo_weak = Rc::downgrade(&foo);
        println!("weak_count: {}", Rc::weak_count(&foo))
    }
    println!("strong_count: {}", Rc::strong_count(&foo));
    println!("weak_count: {}", Rc::weak_count(&foo));
}

输出为:

strong_count: 2
weak_count: 1
strong_count: 1
weak_count: 0

可以看到,每当对应数据离开对应作用域时,会自动清理数据的一些副作用,因此就可以实现Rust独有的数据回收。

结语

此篇文章,粗略的介绍了一下如何使用多所有权,希望能对各位有所帮助。