理解所有权 Ownership

所有权是rust独一无二的特性,使rust在没有GC的情况下保证内存安全

什么是所有权

所有权是一组控制 Rust 程序如何管理内存的规则

所有程序在运行时都必须管理如何使用内存,有些语言使用GC在程序运行时定期处理未使用的内存,有些语言必须显式的分配和释放内存。rust使用第三种方式:通过在编译期检查一系列规则的所有权系统管理内存。若违反规则就不会通过编译

所有权只在编译期检查,不会影响运行期的效率

栈Stack和堆Heap

存在stack上的数据必须是已知,固定的大小。反之会存在heap

当存数据到heap上时,需要先申请指定的空间,然后 memory allocatorheap中找到一个足够大的空间,标记其被使用,然后返回指向这个位置的指针。因为指针是一个固定大小的值,所以该指针可以存在stack上,当需要实际的数据时,还是需要去指针指向的位置获取数据。

stack上存数据是快过于往heap上存数据的,因为stack只需要放到栈顶,而heap需要找到足够大的空间,然后标记被使用,然后返回指针。同样,从stack上取数据也是快过heap

调用函数时,传递进函数的值(包括指向heap数据的指针)和函数的局部变量pushstack,函数结束时这些值将会从stackpop

跟踪代码的哪些部分正在使用heap上的哪些数据,最小化heap上的重复数据,以及清理heap上未使用的数据,这样就不会耗尽内存,这些都是所有权解决的问题

所有权规则

  • rust中每个值都有一个拥有者(owner)
  • 每次只能有一名拥有者
  • 当所有者超出作用域(scope)时,该值将被删除(drop)。

变量作用域(scope)

作用域是程序中某项有效的范围

#![allow(unused)]
fn main() {
    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}
  • s进入作用域时开始有效
  • 保证有效直到离开作用域

String 类型

String存储在heap

从字面量声明一个String

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

1721031795504

图左面的ptr,len,cap存储在stack上,右面的数据存储在heap

上述代码将s1赋值给s2其实是将stack上的ptr,len,cap复制了一份,并不会复制右边heap的实际数据

在将s1赋值给s2后使用s1会报

error[E0382]: borrow of moved value: s1

s1赋值给s2在其他语言中叫shallow copy 浅拷贝,但rust同时让s1无效,在rust中叫move。上述例子就是s1 moved to s2

rust永远不会自动创建深拷贝

深拷贝需要主动调用clone()方法

#![allow(unused)]
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

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

Stack-Only Data: Copy

rust有一个特殊的标识trait Copy,如果一个类型实现了Copy trait,那么它将会被保存在stack上,所以在赋值时是浅拷贝的

实现了droptrait的类型,不能添加Copy trait

以下这些类型都实现了copy trait

  • 所有整数类型,如 i32
  • 布尔类型,如 bool
  • 所有浮点数类型,如 f64
  • 字符类型,如 char
  • 元组,如 (i32, i32),只要元组中所有元素都实现了Copy trait

所有权和函数

函数传递参数类似赋值,传递参数时可能会copy或者move

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.

返回值和作用域

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 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("yours"); // some_string comes into scope

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

// This function takes a String and returns 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
}

变量的所有权始终遵循相同的模式:赋值给新变量时,原变量move

The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it.

当包含heap数据的变量出作用域时,变量将会被drop清理除非其所有权已经moved

引用References 和借用Borrowing

函数每次调用都伴随参数所有权的转移和返回,为了解决这种问题,rust提供了引用(reference)

  • 引用类似指针,可通过它保存的地址访问存储在该地址的数据。和指针不同,引用的整个生命周期保证指向有效的值
  • 引用不会拿走值的所有权
fn main() {
    let s1 = String::from("hello");
    //`&s1`语法创建了一个指向`s1`的值引用,但不拥有`s1`
    let len = calculate_length(&s1);
    //因为没有s1的所有权,函数`calculate_length`结束时`s`也不会`drop`

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

使用&引用,*取引用指向的值

rust称之为引用借用(borrowing),类比现实中的某人有某物,你借用该物,在用完后归还,你从来没有拥有过该物

修改一个引用会直接编译错误

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

    change(&s);
}

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

error[E0596]: cannot borrow *some_string as mutable, as it is behind a & reference

可变(Mutable)引用

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

    change(&mut s);
}

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

&mut T语法创建了一个可变引用mutable reference,允许修改引用所指向的值

但可变引用有一个巨大限制,在已有一个可变引用存在的情况下,不能再创建其他可变引用

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
//报错
error[E0499]: cannot borrow `s` as mutable more than once at a time
}

这个限制可以在编译期防止数据竞争data race

数据竞争在以下情况时发生

  • 2个或更多个指针同时访问相同数据
  • 至少有一个指针被用来写入数据
  • 没有访问数据的同步机制

可以使用创建新的作用域规避这个限制

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

同样,在已有引用存在的情况下,不能在创建可变引用

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{r3}");
}

但支持多个不可变引用

引用的作用域从第一次引用开始到最后一次引用结束

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{r3}");
}

悬空(Dangling)引用

在其它有指针的语言中,很容易错误的创建悬空指针(指向内存可能已被分配给其它指针的指针,如释放内存但保留指针)
rust编译器保证引用永不会悬空,确保引用的作用域内引用的数据始终有效

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!
//编译报错
error[E0106]: missing lifetime specifier

引用规则总结

  • 引用不会拿走所有权
  • 在任何时间,可以有一个可变引用或者多个不可变引用
  • 引用必须总是有效的

切片 Slice

切片允许引用集合中的连续元素序列,而不是使用整个集合。切片也是一种引用,同样不会有所有权

字符串切片

字符串切片就是字符串的部分引用

#![allow(unused)]
fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];// s[..5] == "hello"
    let world = &s[6..11];// s[6..] == "world"
}

创建slice语法为&s[start_index..end_index]

slice长度为end_index - start_index

range..语法

  • 若想从0开始,可以省略start_index,如&s[..end_index]
  • 若想包含String的最后一个字符,可省略end_index,如&s[start_index..](等于&s[start_index..s.len()]
  • 若取整个String,可以全部省略&s[..]

string slice写作&str

#![allow(unused)]
fn main() {
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
}

理解字符串字面量(Literals)

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

s类型实际是&str,这也解释了为什么字符串字面量不可变,因为&str是不可变引用 immutable reference

使用字符串切片替换字符串作为参数

fn first_word(s: &String) -> &str {
//变更为
fn first_word(s: &str) -> &str {
//字符串切片&str替换字符串引用&String让参数变的更灵活

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

    //可接受字符串切片为参数
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    //同样接收字符串引用为参数(和整个字符串切片相等)
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // 也可接收字符串字面量切片
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    //字符串字面量本身就是字符串切片自然可以接收
    let word = first_word(my_string_literal);
}

上述代码利用rust的类型自动转换deref coercions特性

其它类型的Slice

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

上面切片类型为&[i32],原理和字符串切片一样,保存一个指向第一个元素的引用和长度。slice可以用在所有可排序的集合上

总结

所有权(ownership),借用(borrowing),和切片(slice)在编译期确保了rust的内存安全

rust语言可以像其他系统编程语言一样控制内存使用,还可以当数据的所有者超出作用域时自动清理数据