理解所有权 Ownership
所有权是rust独一无二的特性,使rust在没有GC的情况下保证内存安全
什么是所有权
所有权是一组控制 Rust 程序如何管理内存的规则
所有程序在运行时都必须管理如何使用内存,有些语言使用GC在程序运行时定期处理未使用的内存,有些语言必须显式的分配和释放内存。rust使用第三种方式:通过在编译期检查一系列规则的所有权系统管理内存。若违反规则就不会通过编译
所有权只在编译期检查,不会影响运行期的效率
栈Stack和堆Heap
存在stack上的数据必须是已知,固定的大小。反之会存在heap上
当存数据到heap上时,需要先申请指定的空间,然后 memory allocator在heap中找到一个足够大的空间,标记其被使用,然后返回指向这个位置的指针。因为指针是一个固定大小的值,所以该指针可以存在stack上,当需要实际的数据时,还是需要去指针指向的位置获取数据。
往stack上存数据是快过于往heap上存数据的,因为stack只需要放到栈顶,而heap需要找到足够大的空间,然后标记被使用,然后返回指针。同样,从stack上取数据也是快过heap的
调用函数时,传递进函数的值(包括指向heap数据的指针)和函数的局部变量push到stack,函数结束时这些值将会从stack上pop
跟踪代码的哪些部分正在使用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; }
图左面的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),只要元组中所有元素都实现了Copytrait
所有权和函数
函数传递参数类似赋值,传递参数时可能会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_stringas 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语言可以像其他系统编程语言一样控制内存使用,还可以当数据的所有者超出作用域时自动清理数据