Rust:引用
Rust:引用
- 引用
- &T 不可变引用
- &mut T 可变引用
- 借用规则
- Non-Lexical Lifetimes (NLL) 优化
- 解引用
- * 操作符
- 多级引用
- 自动解引用
引用
在C++中,传递参数时我们经常需要在值传递、指针传递、引用传递之间选择。Rust的引用系统在编译期就解决了很多C++运行时才能发现的内存安全问题。
&T 不可变引用
不可变引用允许你读取数据,但不能通过引用修改数据。
语法:
let 引用: &类型 = &变量;
示例:
let s: String = String::from("hello");
let r: &String = &s;println!("原始字符串: {}", s);
println!("引用内容: {}", r);
不可变引用不会获取所有权,原始变量仍然有效。通过&
操作符创建引用,引用只是指向数据的"借用",不会转移所有权。
不可变引用不能修改数据
尝试通过不可变引用修改数据会导致编译错误:
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);
多个不可变引用是安全的,因为它们都只是读取数据,不会造成数据竞争。读取操作本身是线程安全的。
&mut T 可变引用
可变引用允许你通过引用修改数据。
语法:
let 引用: &mut 类型 = &mut 变量;
示例:
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
,编译器不允许这种行为,可变引用使用期间,独占所有权。
这个限制防止了数据竞争。如果允许同时通过原始变量和可变引用访问数据,可能导致数据不一致。
借用规则
Rust的借用规则是其内存安全的核心,这些规则在编译期强制执行,防止数据竞争和内存安全问题。
规则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);
引用必须始终指向有效的内存。这个规则防止了悬垂引用,确保引用永远不会指向已经被释放的内存。
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!
之后不再使用,所以允许后续创建可变引用。这比简单的词法作用域更智能,提高了代码的灵活性。
解引用
* 操作符
使用*
操作符可以访问引用指向的值:
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); // 正确:解引用后比较
必须通过解引用将引用转换为值才能进行操作。
多级引用
可以创建指向引用的引用:
let x = 5;
let r1 = &x; // r1: &i32
let r2 = &r1; // r2: &&i32
let r3 = &r2; // r3: &&&i32println!("x = {}", x);
println!("*r1 = {}", *r1); // 5
println!("**r2 = {}", **r2); // 5
println!("***r3 = {}", ***r3); // 5
每增加一层引用,就需要增加一个*
来解引用。
自动解引用
- 方法调用
在使用.
调用方法时,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).yprintln!("二级引用: ({}, {})", 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
方法
编译器会按照固定顺序尝试,一层一层解引用,直到找到匹配的方法,如果没有匹配的方法,就会报错。