概述
- 参考文档:The Rust Programming Language
Rust不用垃圾回收也不手动释放内存:所有权系统在编译期决定每块内存何时释放,没有运行期开销- 与其他语言的对比:
java靠GC运行期追踪并回收,C/C++靠程序员手动malloc/free,Rust靠编译器检查一组所有权规则,违反规则直接编译不通过 - 一次赋值或传参中,值要么移动、要么被借用、要么被克隆:
flowchart LR A["值绑定到所有者"] -->|"赋值或传参"| B["所有权移动(move)"] A -->|"&T 或 &mut T"| C["借用(borrow)"] A -->|".clone()"| D["深拷贝(clone)"] B --> E["所有者离开作用域"] E -->|"自动 drop"| F["内存释放"] C --> A D --> A
栈与堆
- 栈(
stack):后进先出,只存放编译期已知大小的数据,分配释放只是移动栈指针,速度最快 - 堆(
heap):存放编译期未知大小或可能增长的数据,分配需要查找空闲块并记账,访问要多一次指针跳转 String是典型布局:栈上放三元组(指针, 长度, 容量)指向堆上的字节,drop时释放堆内存
所有权规则
- 每个值有唯一的所有者(某个变量)
- 同一时刻值只能属于一个所有者
- 所有者离开作用域时值被丢弃:编译器自动插入对
drop的调用
移动语义
堆上值的赋值与传参默认是移动(
move)而非复制:原变量交出所有权后立即失效,编译器禁止再使用它rustlet s1 = String::from("hello"); let s2 = s1; // 所有权从 s1 移动到 s2 println!("{}", s1); // 编译错误:s1 已经失效之所以这样设计:若按
java的思路让两个引用指向同一块堆内存,两个变量离开作用域时会对同一块内存释放两次(double free),Rust用移动在编译期排除这个可能Copy类型例外:整型、浮点、bool、char以及全部由Copy类型组成的元组,赋值时自动按位复制,旧变量仍然可用Clone显式深拷贝:对堆上数据想保留原变量时,调用.clone()复制出独立的一份,拷贝发生的位置在调用处直接可见rustlet s1 = String::from("hello"); let s2 = s1.clone(); // s1 与 s2 各持一份堆内存函数传参与返回同样发生移动:值作为参数传入即交出所有权,返回时再移交给调用者
rustfn take(s: String) { } // s 在函数结束时被 drop
借用与引用
传值会交出所有权,函数用完还得还回来太繁琐,引用(
reference)允许访问值而不获取所有权,创建引用称为借用(borrowing)rustfn len(s: &String) -> usize { s.len() } // 借用 s,函数结束后原变量仍可用&T共享引用:只读,可以同时存在任意多个&mut T独占引用:可写,同一时刻最多一个,且与共享引用互斥,被借用的变量本身必须是mutrustlet mut s = String::from("hi"); let r1 = &s; // 共享引用 let r2 = &mut s; // 编译错误:r1 仍存活时不能有可变引用这条规则在编译期排除了数据竞争(
data race)与迭代器失效:一边读一边改的场景根本无法通过编译非词法作用域生命周期(
NLL):引用的存活范围到最后一次使用为止,而不是作用域结束,先后分别使用多个引用是合法的rustlet mut s = String::from("hi"); let r1 = &s; println!("{}", r1); // r1 的最后一次使用 let r2 = &mut s; // 合法:r1 已经不再存活悬垂引用(
dangling reference)被编译期拒绝:函数不能返回指向局部变量的引用,返回值的生命周期必须由入参或'static保证
切片
切片(
slice)是对集合中连续一段的引用,不获取所有权,类型记作&[T]与&str&str:字符串切片,指向一段UTF-8字节并携带长度,字符串字面量的类型是&'static strrustlet s = String::from("hello world"); let hello = &s[0..5]; // &str,指向 s 的一段
生命周期
生命周期的唯一任务:保证引用不会比被引用的值活得久,全部在编译期检查,运行期零开销
绝大多数函数不需要标注:编译器的省略规则(
elision)会自动推断当返回值是引用且编译器无法判断它借用自哪个入参时,需要用
'a显式标明"返回值与哪个入参活得一样久"rustfn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }'a是一个生命周期参数,名字随意('b、'ctx都行),在<>里声明,写法与泛型类型参数<T>一致&'a str中的'a表示这个引用至少在'a这段期限内有效,'a的实际范围由编译器在调用处代入- 同一个
'a出现在两个入参与返回值上,把三者绑成一条约束:返回值的存活范围不能超过两个入参中较短的那个 - 约束在调用点检查,而不是在函数体内:编译器看实参实际能活多久,代入
'a后验证返回值的使用范围放得下
为什么函数体自身不够用:签名是函数对外的契约,编译器检查别的代码调用它时只看签名不看函数体,返回值借用自
x还是y这件事必须写进签名违反约束时的报错示例:
rustlet s1 = String::from("long string"); let result; { let s2 = String::from("hi"); result = longest(&s1, &s2); } // 编译错误:s2 在这里结束,而 result 还要用返回的引用 println!("{}", result);- 报错信息为
borrowed value does not live long enough,意思是被借用的值死得比引用早
- 报错信息为
'a不改变任何引用的实际存活时间,也不产生任何运行期代码,只描述约束供编译器验证两个入参的存活范围确实不同时,可以声明多个参数,返回值只跟它真正借用的那个绑定
rustfn announce<'a, 'b>(text: &'a str, logo: &'b str) -> &'a str { println!("{}", logo); text }结构体持有引用时同样要标注:
'a写进结构体名后面,声明"结构体实例活多久,这个引用就至少得活多久"ruststruct Excerpt<'a> { part: &'a str, }impl块要原样带上:impl<'a> Excerpt<'a> { ... }
&'static str表示引用在整个程序运行期间有效:字符串字面量直接编进二进制,所以它的类型是&'static str实际开发的观感:手写
'a集中在结构体持有引用与返回引用的函数两类场景,其余交给省略规则,遇到报错时按"谁活得太短"排查