Rust学习笔记:变量、数据类型与函数——从let讲起的基础语法

上一篇装好了环境、跑通了 Hello World,这一篇正式进入 Rust 语法。我们先从最基础的三件事开始:变量怎么声明、数据有哪些类型、函数怎么写。这三样在任何语言里都是地基,但 Rust 有一些「反直觉」的设计,值得单独花一篇文章讲清楚。

本文所有代码均已实测通过,每个知识点的运行输出贴在对应小节末尾。


1. let 不可变 vs mut 可变

这可能是 Rust 给新手的第一记闷棍——let 声明的变量默认不可变

let x = 5;
// x = 6;  // ❌ 编译错误:不能给不可变变量赋值

大部分主流语言里,变量天生就能改。Rust 反过来:你要改,得主动声明 mut

let mut y = 5;
y = 6;  // ✅ mut 允许修改

运行输出:

x 初始值: 5
y 初始值: 5  
y 修改后: 6

为什么要这样设计?Rust 追求内存安全,编译期就要确定哪些值会变、哪些不会。默认不可变意味着编译器能做出更激进的优化,也能在编译期就拦住一大类 bug(比如无意中改了不该改的值)。需要可变的时候加个 mut,成本很低,但安全性收益很高。

实用建议:写代码时先用 let,编译器报错说需要可变时再加 mut。这是一个好习惯。


2. 标量类型

标量(scalar)就是单一值,Rust 有四种基本标量:

类型 说明 示例
i32 有符号 32 位整数(默认整数类型) -42
u32 无符号 32 位整数(不能为负) 42
f64 64 位浮点数(默认浮点类型) 3.14
bool 布尔值 true / false
char Unicode 字符(4 字节,不只是 ASCII) '🦀'
let a: i32 = -42;
let b: u32 = 42;
let c: f64 = 3.14;
let d: bool = true;
let e: char = '🦀';  // Rust 的 char 支持 emoji

类型推断:Rust 编译器很聪明,不写类型也能推断:

let inferred_int = 100;    // 推断为 i32
let inferred_float = 2.5;  // 推断为 f64

运行输出:

i32: -42, u32: 42, f64: 3.14, bool: true, char: 🦀
推断的整数: 100, 推断的浮点数: 2.5

关于整数类型的选择:日常写业务逻辑默认用 i32 就够了,性能好且不容易溢出。只有在明确需要非负值(如数组索引、长度)时才用 usize,处理位运算时才考虑 u8/u16 等。这些后续会用到,现在先记住 i32 是默认选项。


3. 复合类型:元组与数组

标量是单个值,复合类型是把多个值组合在一起。

3.1 元组(Tuple)

元素类型可以不同,长度固定,存在栈上:

let tup: (i32, f64, char) = (500, 6.4, 'Z');

// 两种取值方式:
// 方式一:解构
let (x, y, z) = tup;
println!("元组解构: {x}, {y}, {z}");

// 方式二:.索引(从 0 开始)
println!("元组 .0: {}, .1: {}, .2: {}", tup.0, tup.1, tup.2);

3.2 数组(Array)

元素类型必须相同,长度也固定,同样存在栈上:

let arr = [1, 2, 3, 4, 5];
println!("第一个元素: {}", arr[0]);
println!("数组长度: {}", arr.len());

// 快捷创建:let arr = [初始值; 数量]
let zeros = [0; 5];  // [0, 0, 0, 0, 0]

数组的 {:?} 是调试格式输出,打印时用:

println!("zeros 数组: {zeros:?}");  // [0, 0, 0, 0, 0]

运行输出:

元组解构: 500, 6.4, Z
元组 .0: 500, .1: 6.4, .2: Z
数组第一个元素: 1
数组长度: 5
zeros 数组: [0, 0, 0, 0, 0]

数组和元组都是固定长度的,存栈上,访问快。需要动态长度的集合后面会学 Vec(向量)。目前只和固定长度打交道。


4. 函数:参数 & 返回值

Rust 函数用 fn 声明,参数必须标注类型,返回值用 -> 指定:

fn add(a: i32, b: i32) -> i32 {
    a + b  // 最后一个表达式不加分号 → 这就是返回值
}

这里有个 Rust 特有的概念:表达式 vs 语句

  • 语句:执行操作但不返回值,以分号结尾。let x = 5; 是语句。
  • 表达式:计算出一个值,不加分号a + b 是表达式。

所以 add 函数里 a + b 后面没加分号,它的值就是函数的返回值。如果加了分号 a + b;,就变成了语句,函数什么都不返回(实际上是返回 ()——见下文)。

再看一个用 if 当表达式的例子:

fn max(a: i32, b: i32) -> i32 {
    if a > b {
        a  // if 分支,返回 a
    } else {
        b  // else 分支,返回 b
    }
}

这里的 if-else 整体是一个表达式,最后的值就是函数的返回值。这和 C/Java 的三元运算符 a > b ? a : b 类似,但 Rust 不需要单独的语法糖——if 本身就支持。

无返回值的函数

fn print_hello() {
    println!("这是一个无返回值的函数");
}

没有写 -> 返回类型,函数返回的是 (),叫单元类型(unit type)——可以理解为「空」,类似其他语言里的 void

let nothing = print_hello();
println!("print_hello 的返回值: {nothing:?}");  // ()

运行输出:

add(10, 20) = 30
max(5, 9) = 9
这是一个无返回值的函数
print_hello 的返回值: ()

5. const 常量

const 声明的是编译期常量,和 let 有本质区别:

const PI: f64 = 3.1415926;
const MAX_POINTS: u32 = 100_000;  // 下划线提高可读性,编译时会被忽略
对比 let const
类型标注 可选(有类型推断) 必须显式标注
可变性 默认不可变,mut 可变 永远不可变
作用域 可以在函数体内 全局或模块级
计算时机 运行时绑定 编译期确定

简单记:值在编译时就能确定的、全局共享的、永远不变的,用 const;其余用 let

运行输出:

常量 PI: 3.1415926
常量 MAX_POINTS: 100000

6. 变量隐藏(Shadowing)

Shadowing 是 Rust 里一个容易让新手困惑但非常实用的特性:let 重新声明同名变量,新的会「盖住」旧的

let x = 5;
let x = x + 1;  // 新的 x = 6,旧的 x 被隐藏

Shadowing 有两个 mut 做不到的能力:

能力一:跨作用域生效

let x = 5;
{
    let x = x * 2;  // 内部作用域:x = 10
    println!("内部: {x}");  // 10
}
println!("外部: {x}");  // 5——离开作用域,恢复原值

内部用 let 重新绑定的 x 只在花括号内有效,出来就恢复。这和 mut 完全不同——mut 改了就真改了,出作用域也不还原。

能力二:改变类型

let spaces = "   ";       // &str 字符串切片
let spaces = spaces.len(); // usize 数字——完全合法

mut 做不到这件事:

// let mut spaces = "   ";
// spaces = spaces.len();  // ❌ 类型不匹配:usize 不能赋给 &str

Shadowing 本质是创建了一个新变量,只是碰巧和旧的同名。新变量可以有不同的类型,因为它们是两个完全独立的绑定。

运行输出:

第一次 x: 5
第二次 x (shadowing): 6
内部作用域 shadowing: 12
离开内部作用域后 x: 6
"   " 的长度: 3

总结

这一篇覆盖了 Rust 基础语法中最核心的六个概念:

  1. let 默认不可变——Rust 的安全哲学从变量声明就开始了
  2. 四种标量类型——记牢 i32 是默认整数、f64 是默认浮点
  3. 元组和数组——一个混搭、一个同类型,都固定长度存栈上
  4. 函数——参数必带类型,最后一个表达式不加分号就是返回值
  5. const vs let——编译期常量 vs 运行时绑定
  6. Shadowing——mut 做不到的两件事:跨作用域恢复、改变类型

下一篇进入控制流ifloopwhilefor,以及 Rust 最优雅的 match 表达式。

posted @ 2026-09-12 10:14  PC2005-cloud  阅读(7)  评论(0)    收藏  举报