rust之所有权

rust Apr 23, 2021

前言

所有权是rust的核心特性,通过所有权功能,rust能够自动保证内存安全,而不需要垃圾回收器。通过所有权功能,rust避免了内存泄漏,悬挂指针等因为手动管理内存而带来的问题。同时因为不是通过垃圾回收器来实现,性能上也有保证。所有权定义了一套规则在编译器器检查程序来保证程序的内存是安全的。

所有权规则

  1. 每一个堆上的值,都有一个所有者。
  2. 所有者唯一
  3. 当程序运行到所有者的作用域外时,堆上的值将会被删除

变量作用域

rust中作用域的概念和其他编程语言是类似的。比如

  1. 在函数中声明的变量,其作用域只在这个函数中。
  2. 全局变量的作用域是全局
  3. 局部作用域作用在局部

[me]:解释下局部作用域,没找到准确的词汇。他的语法是如下

fn main () {
    let var_main ;
    {            // 作用域开始
        let var = "test".to_string();
        var_main = &var; // 这里我们获取var的引用

    }  // 作用域结束 rust自动调用drop函数清理变量var引用的堆内存
    println!("{}", var_main);  // 这里变量var占用的内存已经被删除了,这里会提示编译错误 `var` does not live long enough
}

ps: 只有分配在堆上的数据才会有所有权。struct,enum,string是分配在堆上的。

move(所有权转移)

let s1 = String::from("hello"); // 字符串值hello的所有者是s1
let s2 = s1; // s2是复制了s1吗? 没有,因为所有权是唯一的,这里s1把所有权转移给的s2,s1失效。
println!("s1 {}", s1); // 编译错误 value borrowed here after move   
println!("s2 {}", s2);

所有权与函数

函数传参的过程就是堆变量所有权转移的过程。


fn main() {
  
    let s = String::from("hello");

    takes_ownership(s); // 所有权转移到了函数的参数里面
}

fn takes_ownership(some_string: String) { // 函数参数的所有权只存在这个函数内
    println!("{}", some_string); // 所有权有效
} // some_string 被删除了


return变量能把所有权归还。


fn main() {

    let mut s  = String::from("hello");

    s = take_and_return_ownership(s); // 所有权转移到了函数的参数里面 然后归还给变量s
    println!("{}", s);
}


fn take_and_return_ownership(some_string: String) -> String { // 函数参数的所有权只存在这个函数内
    println!("{}", some_string); // 所有权有效
    some_string
} // some_string 把所有权归还。当rust发现所有权被转移给了其他变量的时候就不会调用drop函数清理内存了。

引用和借用

在上面的例子中,当我们想要通过函数来使用某个变量,并且在函数处理后变量仍然要使用的时候,需要在把变量return回去,一个变量还好,多了的话,使用起来就很麻烦。另外很多时候我们其实是不需要变量的所有权的,我们只是借用变量。

引用声明

fn main() {
  let s = String::from("hello");
    {
        let s1 = &s; // 我们声明了s1对s的引用

        println!("{}", s1);

    } // s1的作用域结束了,被删除了,但是他没有所有权,所以只是本身被删除,不会把堆上的hello删除。
    println!("{}", s); // s仍人有效;
}

我们可以对比下,如果不使用引用声明的话会怎么样

fn main() {
    let s = String::from("hello");
    {
        let s1 = s; // 所有权转移给了s1

        println!("{}", s1);

    } // s1的作用域结束了,被删除了。
    println!("{}", s); // s无效了 编译报错  value borrowed here after move

}

借用

官网写:引用类型作为函数参数被称为借用。


fn main() {

     let s1 = String::from("hello");

     let len = calculate_length(&s1);
}
fn calculate_length(s: &String) -> usize { 
    s.len()
} 

如果我们想要在函数里面修改s1变量的值怎么办?

可变引用

在&后面加上mut,可以声明为可变引用。

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

那如果我们声明两个可变引用的时候,就会产生竞争问题。这段内存就变得不安全。类似于多线程对一个变量的修改。所以对于引用有如下几个规则

  1. 在同一个作用域里面只能有一个可变引用,可以有多个不可变引用
    [me]:这个有点类似读写锁。可变引用是潜在的写操作,不可变引用是潜在的读操作。读读允许,不用加锁。读写同时存在就有了竞争,但是rust是不允许这种操作。写写也是竞争行为,rust不允许。
  2. 引用必须是有效的。

第二点引出了rust的另外一个特性生命周期。先看一个反例。

fn main() {
    let reference_to_nothing = dangle(); // 从名字就大概猜的出来,在其他语言里面这种叫悬空指针。
}

fn dangle() -> &String {
    let s = String::from("hello"); // 声明变量s,所有权属于s

    &s
} // 糟糕的是 s在函数结尾的时候 drop掉了。 返回的引用指向了一个失效的内存地址。

好在rust是会检查这种情况,并且不允许这种写法。解决这个问题,一种办法是把变量s的所有权转移给函数调用者。一种是声明变量的生命周期。后面一篇我们讲变量的生命周期的声明。

clone

内存竞争的问题可以通过变量的克隆来避免。当我们都操作自己的变量的时候就不会产生竞争的问题,这是一种函数式编程的思想。但是问题就在于,内存操作是有很大的开销的。每次都复制变量到新的内存上,不仅仅是效率会比较低,内存的使用也会很大。所以需要开发者自己做权衡。

ref

  1. https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
  2. https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
  3. https://doc.rust-lang.org/nomicon/ownership.html