1. 引用
很多场景中,我们可能只是想读取某个变量指向的值,并不想获得其所有权,这个时候就可以使用引用。其实在很多其他的编程语言中,也有引用的概念。
- 简单来讲,引用是创建一个变量,指向另个指针的地址,而不是直接指向 该指针指向的堆内存地址
- 通过 & 取地址符获取对一个指针变量的引用
let s1 = String::from("hello");
// 获取 s1 的引用
let s = &s1;
- 下图就很好的表示了 引用 的关系
- 变量s1是栈内存中的一个指针地址,通过 ptr 记录了存储于堆内存中的 String("hello") 的地址
- 变量s也存在栈内存中,通过 ptr 记录了 s1 的指针地址,来实现对 String("hello") 的引用
文章图片
2. 借用 使用引用作为函数参数的行为被称作借用,如何使用借用来规避某个变量的所有权发生移动,我们可以看以下例子:
fn main() {
let s = String::from("hello!");
// 使用 s 的引用作为入参,s 的所有权就不会发生移动
let len = get_length(&s);
println!("the length of {} is {}", s, len);
// the length of hello! is 6
}fn get_length(string: &String) -> usize {
// 引用和变量一样,默认也是不可变的
// string.push_str("world");
// `string` is a `&` reference, so the data it refers to cannot be borrowed as mutablestring.len()
}
3.可变引用 从上面的例子我们可以知道,引用和变量一样,默认也是不可变的,我们不能修改引用所指向的值。但如果想要这么做,那么可以使用 mut 关键字,将引用转为可变的:
fn main() {
let mut s = String::from("hello!");
/*
* 使用 mut 关键字,将 s 的可变引用作为入参,
* 这样 s 的所有权既不会发生移动,函数中也能通过 可变引用 来修改 s 的值
*/
let len = get_length(&mut s);
// 在 get_length 函数中我们实现了对 s 的修改
println!("the length of {} is {}", s, len);
// the length of hello!world is 11
}fn get_length(string: &mut String) -> usize {
// 通过可变引用对值进行修改
string.push_str("world");
string.len()
}
3.1 可变引用的重要限制
- 对应一个指针变量,在一个特定的作用域内,只能有一个可变引用。原因也很好理解,如果在一块作用域内,当一个变量存在两个可变引用,那就意味着同一时间可能有两个变量控制着同一块内存空间,就会发生数据竞争,很容易在运行时产生bug,因此 Rust 通过编译时检查,来规避这样的问题出现
3.1.1 同一作用域,只能存在一个可变引用
通过例子可以看到,当我们对 变量origin 进行了两次可变引用,编译时就直接报错
fn main() {
let mut origin = String::from("hello");
let ref1 = &mut origin;
let ref2 = &mut origin;
// error: cannot borrow `origin` as mutable more than once at a timeprintln!("{}, {}", ref1, ref2);
}
3.1.2 不可变引用可以有多个
如果是同时使用多个不可变引用,则不会有这个限制,因为不可变引用其实就是只读的,不存在可能的内存安全风险。通过这个例子我们可以看到,Rust是允许这样使用的:
fn main() {
let origin = String::from("hello");
let ref1 = &origin;
let ref2 = &origin;
println!("{}, {}", ref1, ref2);
}
3.1.3 某些场景下,可以存在多个可变引用
从上面的两个例子我们已经知道,如果一个作用域内同时存在两个以上的可变引用,那么就可能发生数据竞争,那么是否存在某些场景,会出现多个可变引用呢?我们看下面这个例子:
- 其实在程序执行过程中,只要走出作用域,那么作用域中的变量就会被释放,这个例子中当声明 ref2 时,ref1 已经被销毁了,所以还是可以保证可变引用的第一条原则
fn main() { let mut origin = String::from("hello"); { let ref1 = &mut origin; println!("{}", ref1); }// 当 ref2 声明时,ref1 已经被销毁了 let ref2 = &mut origin; println!("{}", ref2); }
3.1.4 不能同时拥有一个可变引用和一个不可变引用
对于一个指针变量,它的可变引用和不可变引用其实是互斥的,不能同时存在。原因很简单,可变引用可以修改指向内存空间的值,当值被修改,不可变引用的意义也就不存在了。因此 Rust 在编译时会进行检查,发现这种情况则会直接报错:
fn main() {
let mut origin = String::from("hello");
let ref1 = &origin;
let ref2 = &mut origin;
// cannot borrow `origin` as mutable because it is also borrowed as immutableprintln!("{} {}", ref1, ref2);
}
4. 悬垂引用 这一种出现在 C/C++ 等语言中的 bug 场景,描述的是这样一种场景,一个变量所指向的内存空间已经被释放或分配给其他程序使用,但这个变量任然有效。在 Rust 中,编译器会检查这种场景,来避免出现悬垂引用。假设编译通过,下面就是一个会产生悬垂引用的场景:
fn main() {
let s = String::from("haha");
// s 的所有权被移动到 helper 的作用域中
let a = helper(s);
/*
* 假设编译通过:
* 当 helper 调用完毕,s 指向的内存空间就被释放了,但在 helper 中返回了 s 的引用,
* 其实这个引用已经失效,这时 变量a 就变为了一个 悬垂引用
*/
}fn helper(s: String) -> &String {
println!("{}", s);
&s // error: missing lifetime specifier
}
但其实在编译时,Rust就不会允许 helper 返回 s 的引用