【Rust自学】4.4. 引用与借用

发布于:2024-12-21 ⋅ 阅读:(8) ⋅ 点赞:(0)

4.4.0 写在正文之前

这一节的内容其实就相当于C++的智能指针移动语义在编译器层面做了一些约束。Rust中引用的写法通过编译器的约束写成了C++中最理想、最规范的指针写法。所以学过C++的人对这一章肯定会非常熟悉。

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)

4.4.1. 引用

引用让函数使用某个值而不获得其所有权,声明时在类型前加上&即代表引用。例如String的引用就是&String。如果学过C++的话,C++中的解引用符号是*,Rust中也是一样的。

学了引用之后,就可以把上一篇文章最后的示例代码给简化

这是先前的代码:

fn main(){
	let s1 = String::from("hello");
	let (s2, len) = calculate_length(s1);
	println!("The length of '{}' is {}", s2, len);
}

fn calculate_length(s:String) -> (String, uszie) {
	let length = s.len();
	(s, length)
}

这是修改后的代码:

fn main(){
	let s1 = String::from("hello");
	let length = calculate_length(&s1);
	println!("The length of '{}' is {}", s1, length);
}

fn calculate_length(s:&String) -> usize {
	s.len()
}

对比两者,后者中数据的指针被传入函数calculate_length供其操作,而数据所有权依然在变量s1上。不需要返回元组,也不需要再声明一个变量s2,更加简洁。

函数calculate_length的参数s实际上是一个指针,指向s所在栈内存位置(不会直接指向堆内存中的数据)。这个指针在走出作用域时,Rust并不会消除其指向的数据(因为s没有所有权),只会弹出栈上所存储的指针信息,也就是释放下图中的最左侧的部分所占的内存。
请添加图片描述

这种以引用作为函数的参数叫做借用

4.4.2. 借用的特性

借用的内容是不能被修改的,除非是可变引用

以房产为例:你把自己有房产权的房子租给别人就是借用,租户只能住不能乱装修,这就是借用的内容不能被修改的特性;如果你允许租客装修,这就是可变引用。

以这个代码为例:

fn main(){
	let s1 = String::from("hello");
	let length = calculate_length(&s1);
	println!("The length of '{}' is {}", s1, length);
}

fn calculate_length(s:&String) -> usize {
	s.push_str(", world");
	s.len()
}

在编译时这个代码会报错:

error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference

报错的原因在于s.push_str(", world");这一行:引用默认是不可变的,但这一行修改了其数据内容。

引用跟普通的变量声明一样,默认不可变,但加上mut关键字后就可变了:

fn main(){
	let mut s1 = String::from("hello");
	let length = calculate_length(&mut s1);
	println!("The length of '{}' is {}", s1, length);
}

fn calculate_length(s:&mut String) -> usize {
	s.push_str(", world");
	s.len()
}

这样写就不会报错了(但记得在声明s1时把s1声明为可变变量)

这种可以修改数据内容的引用就叫做可变引用

4.4.3. 可变引用的限制

可变引用有两个非常重要的限制,其一是:在特定作用域内,对某一块数据,只能有一个可变的引用。

以这个代码为例:

fn main() {
	let mut s = String::from("hello");
	let s1 = &mut s;
	let s2 = &mut s;
}

因为s1s2都是指向s的可变引用,且在同一个作用域内,所以在编译时会报错:

error[E0499]: cannot borrow `s` as mutable more than once at a time

这么做的目的是防止数据竞争,以下三种条件同时满足时会发生数据竞争:

  • 两个或多个指针同时访问同一个数据
  • 至少有一个指针用于写入数据
  • 没有使用任何机制来同步对数据的访问

在报错信息中提及了at a time,意思为同时(也就是在同一个作用域内)。所以说,只要不同时,也就是两个可变引用在不同的作用域指向同一块数据是可以的。下面的代码就体现了这一点:

fn main() {
	let mut s = String::from("hello");
	{
		let s1 = &mut s;
	}
	let s2 = &mut s;
}

s1s2作用域不相同,所以指向同一块数据是允许的。

可变引用的第二个重要限制是:不可以同时拥有一个可变引用和一个不变的引用 因为可变引用存在的目的是修改数据内容,不变的引用存在的作用就是为了保持数据内容不变,如果两者同时存在,可变引用修改值之后,不可变引用的作用就失效了。

fn main() {
	let mut s = String::from("hello");
	let s1 = &mut s;
	let s2 = &s;
}

因为s1是可变引用,s2是不可变引用,两者出现在同一个作用域指向同一块数据,所以编译器会报错:

error[E0502]: cannot borrow `s` as mutable because it also borrowed as immutable

当然,多个不可变的引用是可以同时出现的

总结:多个读(不可变引用)是可以同时存在的,多个写(可变引用)可以存在但不能同时,多个写和同时读写是不允许的。

4.4.4. 悬空引用(Dangling References)

在使用指针时非常容易引起叫做悬空指针(Dangling Pointer) 的错误,其定义为:一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了。

如果你引用了某些数据,Rust编译器保证在引用离开作用域前数据不会离开作用域。 这是Rust保证悬空引用永远不会出现的做法。

以这个代码为例:

fn main() {
	let r = dangle();
}

fn dangle() -> &String {
	let s = String::from("hello");
	&s
}
  • 创建了一个局部变量 s:
    变量s是一个String,它被分配在栈上,但其底层数据存储在堆上。
  • 返回对s的引用:
    函数最后通过&s返回了s的引用。
  • s 的作用域结束:
    在函数dangle返回后,变量s离开了作用域,根据Rust所有权规则,s的内存被自动释放,&s所指向的内存数据已不再存储s的数据,返回的引用指向的是已经被释放的内存地址,变成了悬空引用(Dangling Pointer)。

Rust的编译器会检查到这一点,在编译时会报错。

4.4.5. 引用的规则

  • 在任何给定的时刻,只能满足下列条件之一:
    • 一个可变的引用
    • 任意数量不可变的引用
  • 引用必须一直有效