Rust 有个很核心的概念叫 ownership ,可以说他决定了变量的生命周期。这个是要写好 Rust 必须得深刻理解的一个能力。
栈( Stack )与堆( Heap )
栈与堆都是 Rust 在内存管理中的一个概念( 在很多高级语言里也有 ),两者的结构不太一样,所以存储数据的方式也就不同。
栈是有序的,遵循先入后出的规则,类似于叠积木。存储在栈里的数据必须是大小固定的。找数据的时候就会从栈的顶端逐个往下找。
堆则是无序的,如果有数据的大小可能会变更,那么这类数据则会存储到堆中,会先在堆里申请一块足够大的内存空间,再把数据写到那个内存空间,拿到内存的地址后再在栈里创建一个指针,指针中会存储该内存空间的地址。所以在堆中查找数据,是需要先在栈中找到指针,再根据指针去找对应的内容。
因此不管是数据写入,还是数据查找,堆都会比栈慢许多,因为在栈中插入数据只需要往最顶部插,找数据也只要从上往下逐个找即可,而堆在数据插入时申请一大块空间会比较耗时,实际写数据的操作也会比栈多几步,而查找数据也是需要先在栈中找到指针,再根据指针的地址信息再去堆里找。
所以在 Rust 中,大小固定的变量,都是会存储在栈中( 比如字符常量( string literal )、整型、 布尔 等),而大小不固定的变量( 字符变量 等 ),则都会内容存储到堆中,然后在栈中维护一条指针。
这里跟 JS 这种动态语言不同,在用 JS 写代码的时候,往往不需要考虑这些,随意定义变量各种赋值,但是在 Rust 中要理解这种不同变量对内存的消耗,才能保证代码的效率够高。
变量的生命周期
Rust 并不具备 JS 那样的 GC 能力,可以追踪变量的使用并且自动释放内存,但是也不会像 C++ 一样完全要用户自己释放,而是有自己的一套内存释放逻辑。
{ // s is not valid here, it’s not yet declaredlet s = "hello"; // s is valid from this point forward// do stuff with s} // this scope is now over, and s is no longer valid
当运行时执行完 scope 之后,s 会自动释放,标记为 invalid 。
释放是会自动调一个 drop 函数,类似于 C++ 里的 RAII 。
变量的 ownership
按照字面意思,ownership 就是变量的所有权。按照前面栈与堆的讲解就可以知道字符变量的存储,会分成指针以及内容,指针在栈里,内容在堆里。所以在 Rust 中的赋值操作,就是新增指针,指向同一份内容。
注意,这里指大小会变的变量,如果是大小固定的那种,是用 copy 的方式,所以不存在 ownership 的转移。
let s1 = String::from("hello");let s2 = s1;

这个比较容易理解是吧,类似于 JS 中的 Object 的引用一样,但是其实有那么一点点不同,除了新增一个指针外,Rust 其实还会把原来的 s1 设置为 invalid ,所以当完成赋值操作后,s2 是可以用的,但是 s1 是 invalid 的!这个就是完成了 ownership 的转移。

let s1 = String::from("hello");let s2 = s1;println!("{}, world!", s1); // throw error,s1 cannot be used anymore
为什么会有这种设计?前面生命周期有说过,当 scope 结束,Rust 会去 drop 掉对应的变量释放内存,但是当存在这种引用之后,如果两个都可用,那么 scope 结束时就会触发两次 drop 同一块内存,会导致异常或者漏洞。
所以 Rust 设计了 ownership 这种概念。也就是堆里的内容,只能被一个 valid 的指针 own ,当该指针赋值给别的变量的时候,就完成了 ownership 的转移,原来那个就不能再用了。
除了这种赋值操作会转移 ownership ,调用函数的时候也可以通过入参转移 ownership
fn main() {let s = String::from("hello"); // s comes into scopetakes_ownership(s); // s's value moves into the function...// ... and so is no longer valid herelet x = 5; // x comes into scopemakes_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 scopeprintln!("{}", 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 scopeprintln!("{}", some_integer);} // Here, some_integer goes out of scope. Nothing special happens.
所以一般情况下,函数的入参如果是存储在 heap 中的变量类型,都是让传引用,比如这样就是没问题,传入的是 mutable 的引用,在函数中对引用进行操作处理。
fn main() {let mut aaa = String::from("aaa");let size = modify_str_size(&mut aaa);println!("{}, {}", &size, aaa); // 6, aaabbb}fn modify_str_size(bbb: &mut String) -> usize {bbb.push_str("bbb");bbb.len()}
变量引用
上面有个特殊的写法 &size 这种,就是引用。如下图所示,s 就是 s1 的引用,写法就是 &s1 。还有可变引用,也就是 &mut s1 。
其中可变引用有个约束就是在某一些特定场景下的同个 scope 内,你只能申明一个可变引用。比如下面这种是会出错。之所以有这样的限制,是为了避免出现数据竞争的情况,即多个引用同时对数据进行变更,Rust 认为这种场景会导致难以 debug ,所以干脆不允许这种场景出现。
let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s; // throw errprintln!("{}, {}", r1, r2);
如果是不同 scope 的,则允许
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;
这种约束也会影响到普通引用和可变引用的混用上,下面这种也会出错。
let mut s = String::from("hello");let r1 = &s; // no problemlet r2 = &s; // no problemlet r3 = &mut s; // BIG PROBLEMprintln!("{}, {}, and {}", r1, r2, r3);
但是,如果普通引用在声明可变引用后再也没有使用了,就不会报错,比如以下这种
let mut s = String::from("hello");let r1 = &s; // no problemlet r2 = &s; // no problemprintln!("{} and {}", r1, r2);// variables r1 and r2 will not be used after this pointlet r3 = &mut s; // no problemprintln!("{}", r3);
空引用异常
这种设计会带来一种空引用异常的场景,就是在函数中定义了一个字符变量,返回的是该字符变量的引用,就会导致函数执行结束跳出 scope ,此前定义的字符变量已经被释放了,但是由于返回了引用给调用方,如果调用方拿引用办事就会跪。不过这种问题基本上在编译的时候都会出错,只是提醒一下有这么个情况
即这种会出错
fn main() {let x = test();}fn test() -> &String {let x = String::from("hello");// throw err ! , cannot return reference&x}
需要这样写才正常,直接返回该字符变量,ownership 会转移到 main 中的 x 。
fn main() {let x = test();}fn test() -> String {let x = String::from("hello");// correctx}
切片类型
let s = String::from("hello world");let world = &s[6..s.len()];
这种产生的效果就是
看到这里,可能会好奇既然 slice 是引用的,那引用后再对 s 做操作会有什么样的效果?
let mut s = String::from("hello world");let world = &s[6..s.len()];s.clear();println!("{}", world); // throw error
Rust 也猜到了,但是其实这么写会报错,因为 s 的这段引用已经“借出去”给 world 了,后面如果 world 还有要用到的话,s 是不允许变更的。
let mut s = String::from("hello world");let world = &s[6..s.len()];println!("{}", world); // corrects.clear();
