Skip to content

变量绑定

使用 let 进行变量绑定(注意,不是赋值)。

在 Rust 中,使用 let 关键字进行变量绑定确实与传统的赋值操作有所不同。尽管在日常用语中,我们可能会互换使用“绑定”和“赋值”这两个词。要理解这两者之间的差异,我们需要深入了解它们的含义以及它们在 Rust 中是如何工作的。

使用 let 关键字创建一个新变量时,你实际上是在将一个名称(标识符)绑定到一个值或者数据结构上。这意味着这个名称现在与这个特定的值关联在一起。这个操作发生在变量首次被引入到作用域时。

rust
let x = 5;

赋值

赋值通常涉及更改已存在的变量的值。在大多数编程语言中,这通过使用 = 操作符来完成。但在 Rust 中,由于默认的不可变性,你需要显式地声明变量为可变的(mut)才能更改其值。

rust
let mut y = 10;
y = 15; // 这是一个赋值操作,将 y 的值更改为 15

为什么说 let 是绑定而不是赋值:

不可变性:Rust 默认的不可变性意味着,除非你显式地声明变量为可变的,否则你不能更改其值。这与许多其他语言不同,在这些语言中,变量可以在任何时候重新赋值。

阴影:在 Rust 中,你可以使用相同的变量名重新绑定变量(即使用 let 关键字)。这称为变量阴影。这允许你使用相同的名称进行新的绑定,而不是在原始变量上进行赋值。

rust
let x = 5;
let x = x + 1; // 这是一个新的绑定,而不是在原始 x 上的赋值

以下是我和 ChatGPT 4.0 的对话:


:我要是在写代码的时候写死了 y = 11,创建了一个不可变的变量,但是我需要更改 y 值,怎么处理这件事情?

ChatGPT

  1. 重新声明为可变

    你可以重新声明 y 为一个可变变量,并赋予其一个新的值。

    当你重新声明一个变量(例如 y)时,你实际上是在创建一个新的变量绑定,这个新的绑定“阴影”了原始的绑定。这意味着新的绑定会覆盖旧的绑定,使其在后续代码中不再可直接访问。

    考虑以下代码:

    rust
    let y = 11;
    let mut y = 22;

    在这里:

    首先,你创建了一个不可变的 y 绑定,其值为 11。然后,你创建了一个新的、可变的 y 绑定,其值为 22。这个新的绑定“阴影”了原始的 y 绑定。从这一点开始,当你引用 y 时,你实际上是引用值为 22 的可变绑定,而不是原始的值为 11 的绑定。原始的 y 绑定仍然存在于内存中(除非被优化掉),但你不能直接访问它,因为新的 y 绑定已经覆盖了它。

    如果你不再使用原始的 y 绑定(或新的 y 绑定),它们最终会在它们的生命周期结束时被丢弃。

  2. 使用阴影

    你可以使用相同的名称 y 创建一个新的变量绑定。这不是真正地更改原始 y 的值,而是创建了一个新的 y 绑定。这称为变量阴影。

    rust
    let y = 11;
    let y = 22; // 这是一个新的 y 绑定,它“阴影”了原始的 y
  3. 使用内部作用域

    你可以在一个新的作用域内重新绑定 y,这样原始的 y 在这个作用域外仍然保持不变。

    rust
    let y = 11;
    {
        let y = 22;
        println!("{}", y); // 输出 22
    }
    println!("{}", y); // 输出 11

:什么是 Rust 的生命周期?

ChatGPT:在 Rust 中,生命周期是一个非常重要的概念,它描述了引用或其他对象在内存中存在的时间段。生命周期确保 Rust 程序的内存安全性,特别是在涉及到引用时。

生命周期主要与引用有关,它们确保:

  1. 引用总是有效的。
  2. 防止悬挂引用(即引用指向已经释放或无效的内存)。
  3. 防止数据竞争和其他并发相关的问题。

以下是有关生命周期的一些核心概念:

  1. 每个引用都有一个生命周期:这是引用所指向的数据在内存中保持有效的时间段。

  2. 隐式生命周期:在许多情况下,Rust 能够隐式地推断引用的生命周期,无需显式地标注。

  3. 显式生命周期:在某些复杂的情况下,你可能需要显式地标注生命周期。这是通过使用生命周期注解来完成的,例如 'a

    rust
    fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
        if s1.len() > s2.len() {
            s1
        } else {
            s2
        }
    }

    在上述函数中,生命周期注解 'a 指定了 s1s2 和返回值都有相同的生命周期。

  • 结构体与生命周期:当你在结构体中存储引用时,你也需要使用生命周期注解。

    rust
    struct Book<'a> {
        title: &'a str,
    }
  • 生命周期的约束:有时,你需要确保某个引用比其他引用存在更长的时间。你可以使用生命周期子类型来指定这些约束。

  • 'static 生命周期:这是 Rust 中最长的生命周期,表示引用的整个程序期间都有效。例如,字符串字面量就有 'static 生命周期。

    rust
    let s: &'static str = "Hello, world!";

总的来说,生命周期是 Rust 中确保内存安全性的关键概念。

通过正确地使用生命周期,你可以避免许多常见的内存相关错误,如悬挂引用或数据竞争。

表达式

几乎所有东西都是表达式(expressions):会返回一个值(value)作为结果。

例外:变量绑定不是表达式

单位类型(unit)表示空,写作 ()

在 Rust 中,单位值 () 也称为 "unit",它是一个特殊的类型,表示没有任何有意义的值。这可以看作是一个零元素的元组。

单位值 () 在 Rust 中有多种用途:

  1. 表示函数没有有意义的返回值:在 Rust 中,每个函数都有返回值,即使你没有明确指定。默认情况下,函数返回单位类型 ()。例如:

    rust
    fn do_nothing() {
        // 此函数没有返回任何有意义的值
    }

    在 Rust 中,fn do_nothing() 定义了一个名为 do_nothing 的函数,该函数不接受任何参数,也不返回任何值。这个函数的主体为空,所以它什么都不做。

需要注意:

  • 类型 () 只有一个可取的值:()
  • () 是默认的返回类型。

例如:

rust
let s = 10;
let tuple_with_one_element = (s,);

在这个例子中,tuple_with_one_element 是一个只包含一个元素 s 的元组。

可以在表达式后加分号 ; 来舍弃它的值,这时它返回 ()。因此,如果一个函数以分号结尾,它返回 ()

举个例子:

首先理解表达式和语句的区别:

  • 表达式(Expression):在 Rust 中,表达式是一段返回值的代码。例如,5 + 6 是一个表达式,它的值是 11
  • 语句(Statement):语句是执行某种操作但不返回值的代码。例如,let x = 5; 是一个语句,它不返回值。

其次是分号的作用:

在 Rust 中,如果你在表达式的末尾加上分号 ;,那么这个表达式就变成了一个语句,它不再返回值。实际上,它返回了一个特殊的值,即单位值 ()

函数返回值:

在 Rust 的函数中,最后的表达式决定了函数的返回值。如果这个表达式后面没有分号,那么该表达式的值就是函数的返回值。如果你在函数的最后一个表达式后面加上分号,那么这个函数就不再返回该表达式的值,而是返回单位值 ()

以下是一个例子来说明这个概念:

rust
// 这个函数返回 i32 类型的值
fn return_value() -> i32 {
    5 + 6
}

// 这个函数没有明确的返回值,因此它返回单位值 ()
fn no_return_value() {
    5 + 6;
}

return_value 函数中,最后的表达式 5 + 6 没有分号,所以它的值 11 是函数的返回值。而在 no_return_value 函数中,5 + 6; 后面有分号,所以它实际上返回了单位值 ()

总之,这段描述的意思是:在 Rust 中,你可以通过在表达式后加分号来舍弃它的值,并使其返回单位值 ()