[Rust 入门]Rust 引用与借用以及生命周期

本教程环境:

系统: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。 如果 btrue,则代码会把它改为指向 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); //          |
}                         // ---------+

'ar 变量的生命周期注解,'bx 变量的生命周期注解。在编译期间借用检查器会比较生命周期的大小。它会发现 r 的生命周期 'ax 的生命周期 '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 存储在一个变量中,该变量的生命周期会延续到实例的末尾,这种限制决定了 'as 的生命周期更长。此时,就产生了矛盾。所以 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
}

省略生命周期

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

  1. 编译器为每个引用参数都分配了一个生命周期参数。
  2. 如果只有一个输入生命周期参数,那么将输出生命周期参数设置为一样的生命周期;
  3. (适用于方法签名,对于一般函数到第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...

参考链接:

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

相关推荐
幸运小圣14 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
老猿讲编程15 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
yezipi耶不耶1 天前
Rust 所有权机制
开发语言·后端·rust
喜欢打篮球的普通人1 天前
rust并发
rust
大鲤余1 天前
Rust开发一个命令行工具(一,简单版持续更新)
开发语言·后端·rust
梦想画家1 天前
快速学习Serde包实现rust对象序列化
开发语言·rust·序列化
数据智能老司机1 天前
Rust原子和锁——Rust 并发基础
性能优化·rust·编程语言
喜欢打篮球的普通人1 天前
Rust面向对象特性
开发语言·windows·rust
上趣工作室1 天前
uniapp中使用全局样式文件引入的三种方式
开发语言·rust·uni-app
许野平1 天前
Rust:GUI 开源框架
开发语言·后端·rust·gui