本教程环境:
系统:MacOS
Rust 版本:1.77.2
上一节了解的 Rust 的所有权机制以及变量的移动操作。移动也就是将所有权进行移动。移动完成之后之前的变量就变成了未初始化的状态。如何这个变量之后还需要使用,就会造成不必要的麻烦。 Rust 提供了一种非拥有型的指针叫做引用 。它是一个地址,可以访问该地址指向的数据。 Rust 把创建对某个值的引用的操作称为借用。
如何使用引用
引用的一个非常典型的用途:允许函数在不获取所有权的情况下访问或操纵某个结构。 在 Rust 中,共享引用是通过 &
运算符显式创建的 ,同时要用 *
运算符显式解引用。
rust
let x = 10;
let r = &x;
assert!(*r == 10);
&mut
创建可变引用。 引用的规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
rust
fn main() {
// 引用
let s1 = String::from("Hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
// The length of 'Hello' is 5.
}
fn calculate_length(s: &String) -> usize {
s.len()
}
上面的 calculate_length
函数中没有使用 *
操作符。这是因为 .
操作符会按需对其左操作数隐式解引用。 s.len()
是 (*s).len()
的简写。
对引用变量赋值
把引用赋值给某个变量会让该变量指向新的地方。
rust
let x = 10;
let y = 20;
let mut r = &x;
if b { r = &y; }
r
最初指向 x
。 如果 b
为 true
,则代码会把它改为指向 y
。
对引用进行引用
Rust 允许对引用进行引用。
rust
struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;
.
操作符会追踪尽可能多层次的引用来找到它的目标。
比较引用
Rust 的比较运算符也能"看穿"任意数量的引用。
rust
let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = ℞
let rry = &ry;
assert!(rrx <= rry);
assert!(rrx == rry);
如果真想知道两个引用是否指向同一块内存,可以使用 std::ptr::eq
,它会将两者作为地址进行比较。
rust
assert!(xx == ry); // 它们引用的目标值相等
assert!(!std::ptr::eq(rx, ry)); // 占据的地址不同
比较运算符的操作数必须具有完全相同的类型。下面代码报错。
rust
assert!(rx == rrx); // 错误:&i32 和 &&i32 的类型不匹配
借用任意表达式结果
Rust 允许借用任意种类的表达式的结果。
rust
fn factorial(n: usize) -> uzise {
(1..n+1).product()
}
let r = &factorial(6);
assert_eq!(r + &1009, 1729);
生命周期
引用看起来像 C 或 C++ 中的普通指针,但普通指针是不安全的,Rust 如何保持对引用的全面控制呢? Rust 中每个引用都有其生命周期, 也就是确保引用有效的作用域。 一旦函数和类型中有了引用,就需要考虑生命周期的问题。
生命周期避免了悬垂引用
悬垂引用:指向了不存在或已经被释放的内存的引用。
不能借用对局部变量的引用并将其移出变量的作用域。
rust
// 生命周期
{
let r;
{
let x = 1;
r = &x;
} // x 被释放,此时 r 引用了一个不存在内存块
assert_eq!(*r, 1);
}
借用检查器
Rust 编译器有一个**借用检查器,**它会比较作用域并确定所有的引用都是有效的。
rust
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
'a
是 r
变量的生命周期注解,'b
是 x
变量的生命周期注解。在编译期间借用检查器会比较生命周期的大小。它会发现 r
的生命周期 'a
比 x
的生命周期 'b
大很多,但是 r
引用了 x
,此时会编译报错。 改成下面的代码会正常编译。
rust
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
将引用用作函数参数
在 Rust 中如果需要将引用作为参数传递,需要为参数指定生命周期。
rust
static mut STASH: &i32 = &10;
fn f(p: &'static i32) {
unsafe {
STASH = p;
}
}
Rust 中全局变量的等价物称为 静态变量(static) 。它在程序启动时就会被创建并一直存续到程序终止时。它的生命周期是全局的,'static
静态生命周期。 也可定义任意生命周期参数。
rust
fn f<'a>(p: &'a i32) { ... }
生命周期 'a
读作 "tick a",是 f
的生命周期参数。<'a>
的意思是"对于任意生命周期 'a
"。指向 p
的引用的生命周期是 'a
,它可以是任何能涵盖对 f
调用的生命周期。
把引用传给函数
函数签名与其调用者的关系是什么呢?
rust
fn g<'a>(p: &'a &i32) { ... }
let x = 10;
g(&x);
看 g
的签名,Rust 就知道它不会将 p
保存在生命周期可能超出本次调用的任何地方:包含本次调用的任何生命周期都必须符合 'a
的要求。所以,Rust 为 &x
选择了尽可能短的生命周期,即调用 g
时候的生命周期。这满足了所有约束:它的生命周期不会超出 x
,并且会涵盖对 g
的完整调用。所以这段代码通过了审核。
返回引用
rust
fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
let mut s = &v[0];
for r in &v[1..] {
if *r < *s {
s = r;
}
}
s
}
包含引用的结构体
如果结构体的字段中使用引用,必须写出它的生命周期。
rust
struct S {
r: &'static i32
}
上面的 r
只能引用生命周期贯穿整个程序的 i32
值。 另一种方法是给类型指定一个生命周期参数 'a
。
rust
struct S<'a> {
r: &'a i32
}
现在 S 类型有了一个生命周期,就像引用类型一样。每创建一个 S 类型的值都会获得一个全新的生命周期 'a
,它会受到该值的使用方式的限制。存储在 r
中的任何引用的生命周期最好都涵盖 'a
,并且 'a
必须比存储在 S 中的任何内容的生命周期都要长。 如下代码实例:
rust
struct S<'a> {
r: &'a i32
}
let s;
{
let x = 10;
s = S { r: &x };
}
assert_eq!(*s.r, 10); // 错误:从已被丢弃的 `x` 中读取
如果创建了一个 S 值,并将 &x
存储在 r
字段中,就会将 'a
完全限制在了 x
的生命周期内部。s = S { r: &x };
会将此 S 存储在一个变量中,该变量的生命周期会延续到实例的末尾,这种限制决定了 'a
比 s
的生命周期更长。此时,就产生了矛盾。所以 Rust 拒绝执行代码。 如果将具有生命周期的类型放置到其他类型中,需要指定生命周期参数。
rust
struct D<'a> {
s: S<'a>
}
不同的生命周期参数
例如:
rust
struct S<'a> {
x: &'a i32,
y: &'a i32
}
下面的代码会出现错误:
rust
let x = 10;
let r;
{
let y = 20;
{
let s = S { x: &x, y: &y };
r = s.x;
}
}
println!("{}", r);
下面来推理上面的过程。
- S 的两个字段具有相同的生命周期。因此 Rust 必须寻找一个同时适合这两个字段的生命周期。
r = s.x
这就要求'a
涵盖r
的生命周期。- 用
&y
来初始化s.y
,要求'a
不能长于y
的生命周期。 此时出现了矛盾,没有哪个生命周期比y
短,但是比r
长。
要解决这个问题,只需要声明两个属性具有各自的生命周期即可。
rust
Struct S<'a, 'b> {
x: &'a i32,
y: &'b i32
}
省略生命周期
符合一些规则的情况下可以省略生命周期注解。 函数或方法的参数的生命周期称为输入生命周期。返回值的生命周期称为输出生命周期 。 生命周期省略规则:
- 编译器为每个引用参数都分配了一个生命周期参数。
- 如果只有一个输入生命周期参数,那么将输出生命周期参数设置为一样的生命周期;
- (适用于方法签名,对于一般函数到第2步即可 )如果方法有多个输入生命周期参数并且其中一个是
&self
或&mut self
,那么所有输出的赋予self
的生命功能周期。
根据这些规则来判断一下是否能够省略。 示例1:
rust
fn first_word(s: &str) -> &str {}
// 根据规则 1,生成
fn first_word<'a>(s: &'a str) -> &str {}
// 符合规则 2,生成
fn first_word<'a>(s: &'a str) -> &'a str {}
// 此时都有了生命周期注解,书写时可以省略
示例2:
rust
fn first_word<'a>(s: &'a str) -> &str {}
// 根据规则 1
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// 根据规则 2, 有两个参数,不满足,所以不可以省略
教程代码仓库:github.com/zcfsmile/Ru...
参考链接:
🌟🌟 🙏🙏感谢您的阅读,如果对你有帮助,欢迎关注、点赞 🌟🌟