Rust中的核心概念之一-Ownership

Posted by ChaosNyaruko on February 22, 2021

什么是Ownership

Ownership是Rust中的一个核心特点,可以理解成是Rust做到内存安全的重要手段。不同于带GC的语言或是C/C++这样显式管理内存的语言,ownership系统实现了在编译期管理内存(但又不需要程序员过分手动介入)从而避免了GC开销。 硬要说的话个人感觉像是C++中的RAII,但是由于带有编译器级别的检查,比C++更不容易出错。

Ownership的规则

  • Rust中所有的值(value)都有一个作为所有者(owner)的变量(variable)
  • (一个值)同时只能有一个owner(参考Rust中赋值的Move语义和在内存管理中起到的作用)
  • 当owner离开作用域(scope)时,其对应的值(value)就会被释放(dropped)(无论是堆上还是栈上的)

解释

Variable Scope

和其他语言类似,变量离开作用域后,就失效了。当一个变量离开作用域时,Rust会自动调用drop 函数,释放内存

Heap and Stack

和C/C++一样,Rust的内存管理也分为堆和栈,栈上内存由操作系统自动管理,堆上内存需要程序员手动管理(Rust中通过Ownership规则和编译器自动插入释放内存的语句,避免像C/C++那样的显式管理)

Move

Rust中的简单赋值=大多类似于其他语言中的浅拷贝,仅拷贝栈上内容。在Rust中,这种变量和数据交互(interact)的方式称为Move。当发生Move(我理解为“实际”所有权的转移)后,原变量在离开作用域后,编译器不会插入drop,从而避免内存的重复释放

这里还引出Rust的另一个设计思想:Rust永远不会“自动”(automatically)去“深拷贝”数据。因此,默认赋值行为的运行时代价都可以认为是比较低的

String结构示例

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

Representation in memory after `s1` has been invalidated

Clone

那么如果我们真的需要深拷贝一块数据需要怎么做呢,在Rust中需要显式调用clone。这实际上也是显式提醒程序员,这里有一段特殊的代码要执行,有可能消耗会比较大

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);
}

Another wrinkle: Stack-Only Data

一些简单数据类型,像整型,我们不调用clone,也不是Move语义,原因在于,这些简单类型在编译期就能确定大小,并且完全存储于栈上,所以拷贝比较快。
Rust中有一种特殊的注解(annotation) Copytrait, 实现了这个trait类型的变量在赋值后依然可以使用。但如果实现了Droptrait,Copy就无法使用
什么样的类型可以实现Copytrait呢?可以参考官方文档,通常来说,没有需要分配资源的类型可以实现。以下是一些实现了Copy的类型

  • 所有整型,如u32
  • Boolean类型,bool
  • 浮点型, 如f64
  • 字符类型,char
  • 元组(Tuples),当然只能包含同样实现Copy的类型,如(i32, i32)实现了,但(i32, String)没有

Functions

函数中传参和返回值也一样有ownership的转移问题,传参即赋值,返回值会将ownship move到调用它的函数

太过仪式化(too much ceremony)和繁琐?-> references

如果严格按照Ownership的规则编写代码,在涉及到函数调用和返回时会显得非常繁琐,感受一下:

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  // moved, so nothing happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("hello"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// takes_and_gives_back will take a String and return one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

如果每个函数,即使是非常简单的都要这么写,未免有些冗杂繁琐,好在Rust提供了reference机制,后面再学

我的看法

ownership 有点像C++的右值和移动语义,但也有所不同,体会下这段代码

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                        // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it’s okay to still
                                    // use x afterward

                                                                                                                            
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
  println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.