掘金 后端 ( ) • 2024-04-21 10:53

本教程环境:

系统:MacOS

Rust 版本:1.77.2

上一节了解的 Rust 的所有权机制以及变量的移动操作。移动也就是将所有权进行移动。移动完成之后之前的变量就变成了未初始化的状态。如何这个变量之后还需要使用,就会造成不必要的麻烦。 Rust 提供了一种非拥有型的指针叫做引用。它是一个地址,可以访问该地址指向的数据。 Rust 把创建对某个值的引用的操作称为借用

如何使用引用

引用的一个非常典型的用途:允许函数在不获取所有权的情况下访问或操纵某个结构。 在 Rust 中,共享引用是通过 & 运算符显式创建的,同时要用 * 运算符显式解引用。

let x = 10;
let r = &x;
assert!(*r == 10);

&mut 创建可变引用。 引用的规则:

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。
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()
}

截屏2024-04-19 14.46.41.png 上面的 calculate_length 函数中没有使用 * 操作符。这是因为 . 操作符会按需对其左操作数隐式解引用。 s.len()(*s).len() 的简写。

对引用变量赋值

把引用赋值给某个变量会让该变量指向新的地方。

let x = 10;
let y = 20;
let mut r = &x;

if b { r = &y; }

r 最初指向 x。 如果 btrue,则代码会把它改为指向 y

对引用进行引用

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 的比较运算符也能"看穿"任意数量的引用。

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,它会将两者作为地址进行比较。

assert!(xx == ry); // 它们引用的目标值相等
assert!(!std::ptr::eq(rx, ry)); // 占据的地址不同

比较运算符的操作数必须具有完全相同的类型。下面代码报错。

assert!(rx == rrx); // 错误:&i32 和 &&i32 的类型不匹配

借用任意表达式结果

Rust 允许借用任意种类的表达式的结果。

fn factorial(n: usize) -> uzise {
	(1..n+1).product()
}
let r = &factorial(6);
assert_eq!(r + &1009, 1729);

生命周期

引用看起来像 C 或 C++ 中的普通指针,但普通指针是不安全的,Rust 如何保持对引用的全面控制呢? Rust 中每个引用都有其生命周期, 也就是确保引用有效的作用域。 一旦函数和类型中有了引用,就需要考虑生命周期的问题。

生命周期避免了悬垂引用

悬垂引用:指向了不存在或已经被释放的内存的引用。

不能借用对局部变量的引用并将其移出变量的作用域。

// 生命周期
{
    let r;
    {
        let x = 1;
        r = &x;
    } // x 被释放,此时 r 引用了一个不存在内存块
    assert_eq!(*r, 1);
}

截屏2024-04-19 15.00.54.png

借用检查器

Rust 编译器有一个**借用检查器,**它会比较作用域并确定所有的引用都是有效的。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

'ar 变量的生命周期注解,'bx 变量的生命周期注解。在编译期间借用检查器会比较生命周期的大小。它会发现 r 的生命周期 'ax 的生命周期 'b 大很多,但是 r 引用了 x,此时会编译报错。 改成下面的代码会正常编译。

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

将引用用作函数参数

在 Rust 中如果需要将引用作为参数传递,需要为参数指定生命周期。

static mut STASH: &i32 = &10;
fn f(p: &'static i32) {
    unsafe {
        STASH = p;
    }
}

Rust 中全局变量的等价物称为 静态变量(static)。它在程序启动时就会被创建并一直存续到程序终止时。它的生命周期是全局的,'static 静态生命周期。 也可定义任意生命周期参数。

fn f<'a>(p: &'a i32) { ... }

生命周期 'a 读作 “tick a”,是 f 的生命周期参数。<'a> 的意思是“对于任意生命周期 'a”。指向 p 的引用的生命周期是 'a,它可以是任何能涵盖对 f 调用的生命周期。

把引用传给函数

函数签名与其调用者的关系是什么呢?

fn g<'a>(p: &'a &i32) { ... }
let x = 10;
g(&x);

g 的签名,Rust 就知道它不会将 p 保存在生命周期可能超出本次调用的任何地方:包含本次调用的任何生命周期都必须符合 'a 的要求。所以,Rust 为 &x 选择了尽可能短的生命周期,即调用 g 时候的生命周期。这满足了所有约束:它的生命周期不会超出 x,并且会涵盖对 g 的完整调用。所以这段代码通过了审核。

返回引用

fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
    let mut s = &v[0];
    for r in &v[1..] {
        if *r < *s {
            s = r;
        }
    }
    s
}

包含引用的结构体

如果结构体的字段中使用引用,必须写出它的生命周期。

struct S {
	r: &'static i32
}

上面的 r 只能引用生命周期贯穿整个程序的 i32 值。 另一种方法是给类型指定一个生命周期参数 'a

struct S<'a> {
	r: &'a i32
}

现在 S 类型有了一个生命周期,就像引用类型一样。每创建一个 S 类型的值都会获得一个全新的生命周期 'a,它会受到该值的使用方式的限制。存储在 r 中的任何引用的生命周期最好都涵盖 'a,并且 'a 必须比存储在 S 中的任何内容的生命周期都要长。 如下代码实例:

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 存储在一个变量中,该变量的生命周期会延续到实例的末尾,这种限制决定了 'as 的生命周期更长。此时,就产生了矛盾。所以 Rust 拒绝执行代码。 如果将具有生命周期的类型放置到其他类型中,需要指定生命周期参数。

struct D<'a> {
	s: S<'a>
}

不同的生命周期参数

例如:

struct S<'a> {
	x: &'a i32,
	y: &'a i32
}

下面的代码会出现错误:

let x = 10;
let r;
{
    let y = 20;
    {
        let s = S { x: &x, y: &y };
        r = s.x;
    }
}
println!("{}", r);

截屏2024-01-18 09.56.54.png 下面来推理上面的过程。

  • S 的两个字段具有相同的生命周期。因此 Rust 必须寻找一个同时适合这两个字段的生命周期。
  • r = s.x 这就要求 'a 涵盖 r 的生命周期。
  • &y 来初始化 s.y,要求 'a 不能长于 y 的生命周期。 此时出现了矛盾,没有哪个生命周期比 y 短,但是比 r 长。

要解决这个问题,只需要声明两个属性具有各自的生命周期即可。

Struct S<'a, 'b> {
	x: &'a i32,
	y: &'b i32
}

省略生命周期

符合一些规则的情况下可以省略生命周期注解。 函数或方法的参数的生命周期称为输入生命周期。返回值的生命周期称为输出生命周期生命周期省略规则:

  1. 编译器为每个引用参数都分配了一个生命周期参数。
  2. 如果只有一个输入生命周期参数,那么将输出生命周期参数设置为一样的生命周期;
  3. (适用于方法签名,对于一般函数到第2步即可)如果方法有多个输入生命周期参数并且其中一个是 &self&mut self,那么所有输出的赋予 self 的生命功能周期。

根据这些规则来判断一下是否能够省略。 示例1:

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:

fn first_word<'a>(s: &'a str) -> &str {}
// 根据规则 1
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// 根据规则 2, 有两个参数,不满足,所以不可以省略

教程代码仓库:https://github.com/zcfsmile/RustLearning/tree/main/reference

参考链接:

🌟🌟 🙏🙏感谢您的阅读,如果对你有帮助,欢迎关注、点赞 🌟🌟