Rust作为一门新兴的编程语言,与其他编程最不一样的就是变量的所有权与生命周期,而这两项又是保证Rust语言安全的基础。
所有权
在大多数编程语言中,对于编译期能够确定大小的类型,一般存放在栈上;编译期无法确定大小的类型,一般都会存储在堆上。在程序执行时比如赋值、传参、返回时,为了避免数据拷贝,会选择传递引用,而不是直接传递数据。所以会遇到堆内存被多次引用的问题,这些引用做什么操作,什么时候释放,都是需要考虑的问题。
C中要求程序员自己管理内存,自行负责内存的申请与释放;Java等语言到处都是按引用传参,使用追踪式GC来回收内存,有性能损耗且存在stop the world问题;Objective-C/Swift使用自动引用计数来管理内存,但是也会有性能开销。
而Rust语言选择了一条不同的路,单一所有权,通过所有权来管理内存,避免了上述问题,为了保证所有权的独占,Rust语言的所有权规则如下:
- 一个值只能有一个对应的变量来“拥有”它,这个变量被称为“所有者”。
- 一个值同一时刻只能有一个所有者。
- 当所有者离开作用域,这个值将被释放。
fn main() {
let s = String::from("hello");
let s1 = s; // s的所有权转移给了s1, s不再有效
// println!("{}", s); // 此处会报错,因为s已经不再有效
println!("{}", s1);
// s1释放
}
上面的例子中,当把s赋值给s1时,s的所有权转移到了s1,s不再有效,也就是Move语义,以确保字符串hello
在同一时刻只能有一个所有者。当s1离开作用域时,s1的所有权释放,字符串hello
也随之释放。
但是这里如果数据是存储在栈上的简单数据,比如整型、浮点型、布尔型等,这都需要转移所有权,又太复杂了吧。所以Rust语言对于这些简单数据类型,采用了Copy trait来实现,这样就是复制,而不是转移所有权。
fn main() {
let x = 5;
let y = x; // 此时x和y都是有效的,因为x是简单数据类型,采用了Copy trait
println!("x = {}, y = {}", x, y);
// y释放
// x释放
}
Copy语义与Move语义
Rust语言中,对于实现了Copy trait的类型,在赋值或者传参时,值会自动按位拷贝;而对于没有实现Copy trait的类型,会采用Move转移所有权的方式来传递数据。
实现了Copy trait的类型:
- 原生类型:整型(
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,i128
,u128
,isize
,usize
)、浮点型(f32
,f64
)、布尔型、字符型(char
)、单元类型()
、Never Type(!
)。 - 不可变引用(&T)
- 函数指针
- 裸指针(
*const T
,*mut T
) - 数组
[T;N]
、元组(T1, T2, ..., Tn)
、Option<T>
类型(需要注意的是:只有当它们的元素类型都实现了Copy trait时,它们才实现了Copy trait)。
对于复合类型,比如枚举体和结构体,Rust语言默认是不实现Copy trait的,但是如果这些类型的所有成员都实现了Copy trait,那么你可以手动添加#[derive(Copy, Clone)]
来实现Copy trait。如果内部结构包含Move语义的类型,那么就无法实现Copy trait。
// 此时因为未实现Copy trait,所以在赋值或者传参时,会采用Move语义
#[derive(Debug)]
struct A {
a: i32,
b: u32,
}
fn main() {
let a = A{a: 1, b: 2};
let b = a;
println!("{:?}", a); // 此处会报错,因为a已经不再有效
}
// 我们为struct A添加了Copy trait,这是就是采用Copy语义了
#[allow(dead_code)]
#[derive(Debug, Copy, Clone)]
struct A {
a: i32,
b: u32,
}
fn main() {
let a = A{a: 1, b: 2};
let _b = a;
println!("{:?}", a);
}
借用(Borrow)
fn main() {
let s = String::from("hello");
print(s);
// println!("{}", s); // 此处会报错,因为s已经在函数传参时转移了所有权,不再有效
}
fn print(s: String) {
println!("{}", s);
}
那如果是像上面这种,函数传参后,还需要使用到字符串,没有实现Copy trait,但是又不想转移所有权,这时就需要借用了。Rust语言中,借用分为可变借用和不可变借用,可变借用的语法是&mut
,不可变借用的语法是&
。
fn main() {
let mut s = String::from("hello");
print(&s);
add(&mut s);
println!("{}", s); // 输出:hello world
}
// 这里不对字符串进行修改,所以用不可变借用
fn print(s: &String) {
println!("{}", s);
}
// 这里对字符串进行修改,所以用可变借用
fn add(s: &mut String) {
s.push_str(" world");
}
需要说明的是,这里使用的是借用
这个词,而不是引用
,因为Rust语言中的引用和其他语言中的引用不一样,Rust语言中的引用是指向某个值的指针,而不是别名。在其他语言中,引用就相当于拿到了值的别名,跟原来的值是同一个东西,可以进行任何无差别访问。
而在Rust语言中的借用都是临时借用使用权,并不破坏单一所有权原则,好比我从你那里借了一本书,但是我肯定不能随随便便在上面乱写乱画,因为书还是归你所有,用完之后就会归返。所以Rust的借用默认是不可变的,如果想要修改引用的值,需要使用可变借用(取得书主人的授权)。
借用的限制
为了保证内存安全,Rust语言中的借用也有一些限制,比如:
- 在同一作用域中,同一数据只能有一个可变借用。
- 在同一个作用域中,同一数据可以有多个不可变借用
- 在一个作用域中,可变借用与不可变借用不能同时存在
- 所有借用的生命周期不能超出值的生命周期。
前面的三条主要是为了保证内存安全,核心原则就是:共享不可变,可变不共享。
最后一条主要是为了防止悬垂指针。
fn main() {
let s = get_str();
println!("{}", s);
}
fn get_str<'a>() -> &'a str {
let s = String::from("hello");
&s // 返回s的引用
}
上面的例子中,get_str
函数返回了s
的引用,但是s
在get_str
函数结束之后就会被释放,也就是借用超出了值的生命周期,所以这里是不合法的,编译器会报错。
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:8:5
|
8 | &s // 返回s的引用
| ^^ returns a reference to data owned by the current function
For more information about this error, try `rustc --explain E0515`.
生命周期
上面提到了生命周期,而在Rust中,生命周期是用来解决借用的限制问题的。生命周期是指在程序运行时,某个值的有效范围。生命周期的作用是保证借用的值在借用结束之前有效。
有些值具有静态生命周期,它们会在整个程序运行期间一直有效。比如:
- 全局变量
- 静态变量
- 字符串字面量
- 函数指针
- &‘static T类型的引用
而其他的类型,比如局部变量,函数参数,函数返回值等,都是具有动态生命周期的,它们的生命周期是在函数调用时确定的。
生命周期参数
在Rust中,生命周期参数是用来描述引用的生命周期的,它的语法是'a
,其中a
是生命周期参数的名称,可以是任意的合法标识符。生命周期参数位于引用符号&
和引用的类型之间,比如:
&i32 // 引用
&'a i32 // 标注生命周期参数的不可变引用
&'a mut i32 // 标注生命周期参数的可变引用
标注的生命周期参数只是用于编译器的借用检查。
函数签名中的生命周期参数:
fn foo<'a, 'b: 'a> (x: &'a str, y: &'b str) -> &'a str;
其中'b: 'a
的意思是生命周期参数'b
的生命周期至少要大于等于'a
的生命周期,也就是说'b
的生命周期不能小于'a
的生命周期。
条件限制:
- 函数签名中输入(出借方)的生命周期长度必须大于等于输出(借出方)的生命周期长度
- 禁止没有任何输出参数的情况下,返回引用;主要是避免造成悬垂指针。
结构体中的生命周期参数:
struct Foo<'a> {
x: &'a str,
}
impl <'a> Foo<'a> {
fn new(s: &'a str) -> Self {
Foo { x: s }
}
fn foo(&self) -> &'a str {
self.x
}
}
条件限制:
- 结构体实例的生命周期应该短于或等于任意一个成员的生命周期
省略生命周期参数规则
Rust编译器会根据一些规则来自动推导生命周期参数,这些规则称为省略生命周期参数规则。
- 每一个引用类型都有独立的生命周期参数,比如
'a
,'b
等 - 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
- 如果存在多个输入生命周期参数,但是其中一个参数是
&self
或者&mut self
,那么self
的生命周期被赋予所有输出生命周期参数