引用与借用
在C++中,传递参数时我们经常需要在值传递、指针传递、引用传递之间选择。Rust的借用系统在编译期就解决了很多C++运行时才能发现的内存安全问题。
获取变量的引用,这个行为叫做借用,正如现实中一样,可以像别人借一样东西过来,那么此时这个东西的使用权就在我手里,其他人想再问拥有者借用,是借不到的,而且我使用完毕后也必须要物归原主
1.引用与解引用
常规引用是一个指针类型,指向了被引用对象的内存地址,获取一个变量的引用需要使用&操作符,&x也就是获取x的内存地址
想要获取这个指针指向地址中的值,要使用解引用操作符*
let x = 5;
let y = &x; // 把x的地址传递给y
println!("{}", x); // 5
println!("{}", *y); // 5上面代码可以看到,获取变量的引用,是不会获取所有权的
就像我问你借一样东西,这个东西现在在我手里,但它毕竟是你的东西
TIP
上面的代码,如果我们直接打印y,而不是*y
let x = 5;
let y = &x;
println!("{}", y); // 5可以看到,打印出来的值仍然是五,按道理来说,按c嘎嘎的逻辑来说,此时y应该是个地址
如果我们判断y==*y来打印,是会报错的
因为println宏为了方便,会自动解引用,如果想打印真正的地址,需要把占位符{}换成{:p}
let x = 5;
let y = &x;
println!("{:p}", &x); // 0xfa802ffb34
println!("{:p}", y); // 0xfa802ffb34这样就可以得到一个正常的地址了
注意每次程序执行打印的地址都不是一样的,因为当程序执行完毕,内存会被释放,那么原先的地址就可能会被别的程序占用。再次执行时,程序会重新找可用的内存地址
2.不可变引用
上面所说的引用,就是不可变引用
不可变引用允许你读取数据,但不能通过引用改变数据。
语法:
let num = 666;
let borrow = #
// 带类型标注
let borrow: &u32 = #如下示例
let s: String = String::from("hello");
let r: &String = &s;
println!("原始字符串: {}", s);
println!("借用内容: {}", r);不可变引用不会获取所有权,原始变量仍然有效。通过&操作符创建借用,借用只是指向数据的指针,相当于一个别名,你可以同时通过r和c访问这个字符串。
不可变借用不能修改数据
尝试通过不可变借用修改数据会导致编译错误:
let mut s = String::from("hello");
let r = &s;
r.push_str(", world"); // 编译错误!这个限制确保了数据的不变性。即使原始变量是mut的,通过不可变借用也无法修改数据,这是Rust类型系统的核心安全保证。
多个不可变借用可以同时存在
可以同时创建多个不可变借用:
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, {}", r1, r2, r3);多个不可变借用是安全的,因为它们都只是读取数据,不会造成数据竞争。读取操作本身是线程安全的。
3.可变引用
可变借用允许你通过引用修改数据。
语法:
let mut num = 666;
let borrow = &mut num;
// 带类型标注
let borrow: &mut u32 = &mut num;示例:
let mut s = String::from("hello");
let r: &mut String = &mut s;
r.push_str(", world");
println!("修改后: {}", r);可变借用在其生命周期内独占访问权。注意原始变量必须声明为mut才能创建可变借用。
可变借用存在时原始变量不可访问
当可变借用存在时,不能同时使用原始变量:
let mut s = String::from("hello");
let r = &mut s;
println!("{}", s); // 编译错误!
r.push_str(", world");以上代码中,r 获取了字符串的可变借用,同时用户使用了原始变量 s,编译器不允许这种行为,可变借用使用期间,独占所有权。
这个限制防止了数据竞争。如果允许同时通过原始变量和可变借用访问数据,可能导致数据不一致。
3.借用
3.1 借用的含义
把一个变量的引用赋值给另一个变量,这个行为叫做借用
3.2 借用规则
规则1:同一时间只能有一个可变借用
正确的使用方式:
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1);违反规则的错误示例:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误
println!("{}, {}", r1, r2);这个规则防止了数据竞争。如果允许多个可变借用同时存在,可能导致一个借用修改数据时,另一个借用读取到不一致的状态。
规则2:可变借用与不可变借用不能同时存在
正确的使用方式:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);违反规则的错误示例:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 编译错误!
println!("{}, {}, {}", r1, r2, r3);这个规则防止不可变借用的数据被意外修改。如果允许可变借用和不可变借用同时存在,不可变借用持有者期望数据不变,但可变借用可能会修改数据,导致不可变借用看到意外的数据变化。
规则3:借用的生命周期不能超过其借用的数据
正确的生命周期管理:
let mut s = String::from("hello");
{
let r1 = &s;
println!("{}", r1);
} // r1 在这里超出作用域
let r2 = &mut s;
r2.push_str(", world");
println!("{}", r2);违反生命周期规则的错误示例:
let r;
{
let x = 5;
r = &x; // 编译错误:x 的生命周期不够长
}
println!("{}", r);借用必须始终指向有效的内存。这个规则防止了悬垂借用,确保借用永远不会指向已经被释放的内存。
4.NLL优化
Non-Lexical Lifetimes (NLL) 优化是Rust 2018引入了更智能的借用检查,能够分析借用的实际使用范围:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在这里不再被使用,生命周期结束
let r3 = &mut s; // 现在可以创建可变借用
println!("{}", r3);编译器能够分析出r1和r2在println!之后不再使用,所以允许后续创建可变借用。这比简单的词法作用域更智能,提高了代码的灵活性。
5.函数中的借用
之前说过函数中的参数传递,就相当于let,所以函数中也是可以用引用来获取参数
在所有权中我们说到,当一个String字符串被传入了一个函数中,那么在函数调用完后,这个String就不可再用了,但是使用借用就可以解决这个问题
5.1 不可变引用参数
给函数从参数创建不可变引用参数
- 需要在函数的参数列表中,在参数类型的前面添加
&操作符 - 在函数使用时,也要给实参前面添加
&操作符
fn get_str(str: &String) {
println!("{}", str);
}
fn main() {
let s: String = String::from("你好");
get_str(&s);
println!("{}", s); // "你好" //仍可以使用
}5.2 可变引用参数
创建可变引用参数,
需函数参数列表中,在类型和
&中间添加mut关键字在函数使用时,也要在参数和
&中间添加mut关键字并且变量本身也要是可变的
fn get_str(str: &mut String) {
str.push_str(",我是蔡徐坤");
}
fn main() {
let mut s: String = String::from("你好");
get_str(&mut s);
println!("{}", s); // 你好,我是蔡徐坤
}6.解引用
6.1 * 操作符
使用*操作符可以访问借用指向的值:
let x = 5;
let y = &x;
println!("x = {}", x);
println!("*y = {}", *y);解借用操作符*获取借用指向的实际值,这是最基础的解借用方式。
借用和值是不同的类型,不能直接比较
let x = 5;
let y = &x;
assert_eq!(5, y); // 编译错误:不能比较i32和&i32
assert_eq!(5, *y); // 正确:解借用后比较必须通过解借用将借用转换为值才能进行操作。
6.2 多级借用
可以创建指向借用的借用:
let x = 5;
let r1 = &x; // r1: &i32
let r2 = &r1; // r2: &&i32
let r3 = &r2; // r3: &&&i32
println!("x = {}", x);
println!("*r1 = {}", *r1); // 5
println!("**r2 = {}", **r2); // 5
println!("***r3 = {}", ***r3); // 5每增加一层借用,就需要增加一个*来解借用。
6.3 自动解借用
方法调用
在使用.调用方法时,Rust会自动解借用:
let s = String::from("hello");
let r1 = &s;
let r2 = &r1;
let r3 = &r2;
// 以下调用都等价
println!("直接调用: {}", s.len());
println!("一级借用: {}", r1.len()); // 等价于 (*r1).len()
println!("二级借用: {}", r2.len()); // 等价于 (**r2).len()
println!("三级借用: {}", r3.len()); // 等价于 (***r3).len()多级借用时,方法调用会自动解借用到找到对应方法为止。
- 字段访问
访问结构体字段时也会自动解借用:
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 10, y: 20 };
let r1 = &point;
let r2 = &r1;
// 以下访问都等价
println!("直接访问: ({}, {})", point.x, point.y);
println!("一级借用: ({}, {})", r1.x, r1.y); // 等价于 (*r1).x, (*r1).y
println!("二级借用: ({}, {})", r2.x, r2.y); // 等价于 (**r2).x, (**r2).y
}如果把变量放在函数 () 内作为参数传入,那么此时必须手动解借用。
let x = 5;
let r1 = &x;
let r2 = &r1;
// 方法调用:自动解借用
let abs1 = x.abs(); // 直接调用
let abs2 = r1.abs(); // 自动解借用
let abs3 = r2.abs(); // 自动多重解借用
// 函数调用:需要手动解借用
let abs4 = i32::abs(x); // 传值
let abs5 = i32::abs(*r1); // 手动解借用
let abs6 = i32::abs(**r2); // 手动多重解借用
println!("方法调用结果: {}, {}, {}", abs1, abs2, abs3);
println!("函数调用结果: {}, {}, {}", abs4, abs5, abs6);以上代码中,用两种不同的方式调用了相同的函数abs,一个需要手动解借用,另一个自动解借用。
关键区别:
obj.function():编译器自动处理所有层级的解借用function(*obj):需要手动解借用确保参数类型匹配
自动解借用是编译器自动完成的,编译器查找方法时的步骤:
let s = String::from("hello");
let r = &s;
let rr = &r;
let len = rr.len();
println!("字符串长度: {}", len);代码中,定义了一个二级借用 rr,并调用了 rr.len() 方法。此时编译器会进行尝试:
rr.len()不存在,rr这个二级借用没有len方法*rr.len()不存在,*rr这个一级借用没有len方法**rr.len()存在,**rr这个String有len方法
编译器会按照固定顺序尝试,一层一层解借用,直到找到匹配的方法,如果没有匹配的方法,就会报错。
非常好,这篇文章的结构已经很清晰:从借用规则、生命周期到自动解借用,层层递进。 直接在这套逻辑下补充“切片(slice)”内容——最自然的落脚点,就是放在借用之后,因为切片本质上就是“借用的一部分连续元素”。下面是一个与你现有行文风格、语气、深度完全一致的补充章节。
