5.1 函数、元组与表达式
在前两章中,我们已经确立了「一切皆类型」的视角。在动手定义结构体(struct)和枚举(enum)之前,我们先要把代码里最基础的组织骨架——函数、元组与控制流(if / else、while、loop)——一次性立起来。
顺着类型的视角看过去,你会发现 Rust 的函数与控制流比其他语言更加统一:没有 void 特例,而且连代码块 {}、if / else 和 loop 本身都有确定的类型。
函数、元组与单元类型 ()
Section titled “函数、元组与单元类型 ()”先看 Rust 中最常见的三类函数返回形式:返回单个值、用元组返回多个值,以及不返回业务数据。
1. 函数签名:参数与返回类型不能省略
Section titled “1. 函数签名:参数与返回类型不能省略”Rust 用 fn 关键字定义函数,函数名遵循蛇形命名法(snake_case):
fn add(a: i32, b: i32) -> i32 { a + b}- 参数列表:写成
参数名: 类型,多个参数用逗号分隔(如a: i32, b: i32); - 返回值类型:写在箭头
->之后(如-> i32)。
在函数体内部,局部变量可以省略类型标注,由编译器自动推断;但在函数签名上,所有参数和返回值的类型必须显式写出。
原因在于编译期检查的隔离性:调用方只看这一行签名就能核对类型与所有权交接,无需翻看函数体内部;函数体内部的修改只要不改变签名,也绝不会破坏外部调用方的代码。
2. 元组(Tuple):打包多个值与解构
Section titled “2. 元组(Tuple):打包多个值与解构”当函数需要同时返回两个或更多值,又不必专门定义一个结构体时,可以用圆括号把它们打包成元组(Tuple):
fn min_max(a: i32, b: i32) -> (i32, i32) { if a < b { (a, b) } else { (b, a) }}元组长度在编译期固定;若内部元素都实现了 Copy,元组自身也自动实现 Copy。拿到元组后有两种取值方式:
let pair: (i32, i32) = min_max(9, 2);let (low, high) = pair; // 1. 模式解构:拆成两个独立变量let max_val = pair.1; // 2. 点号加下标:取第 1 个元素3. 单元类型 ():为什么 Rust 没有 void?
Section titled “3. 单元类型 ():为什么 Rust 没有 void?”如果一个元组里一个元素都不放,写出来就是一对空括号 (),在 Rust 中称为单元类型(Unit Type),它唯一的值也写作 ()。
fn log_ready(tag: &str) { println!("[{tag}] ready");}在 C 或 Java 中,这类不返回业务数据的函数要标为 void(代表「没有类型」的特例)。而 Rust 取消了 void——省略了 -> ReturnType 的函数,实际上全部默认返回单元类型 -> ()。
运行下面这段完整代码,核对这三类函数的输出以及 () 在内存中的字节大小:
fn-and-unit.rs
// Uncomment the marked line (and comment out `a + b`) to trigger error[E0308].use std::mem::size_of;
fn add(a: i32, b: i32) -> i32 { a + b // a + b;}
fn min_max(a: i32, b: i32) -> (i32, i32) { if a < b { (a, b) } else { (b, a) }}
fn log_ready(tag: &str) { println!("[{tag}] ready");}
fn main() { let sum: i32 = add(3, 4); let pair: (i32, i32) = min_max(9, 2); let (low, high) = pair; let unit_val: () = log_ready("init");
println!( "sum = {sum}, min = {low}, max = {high} (or {}), unit = {:?}, size of () = {}B", pair.1, unit_val, size_of::<()>() );}运行输出:
运行输出[init] readysum = 7, min = 2, max = 9 (or 9), unit = (), size of () = 0B从输出 size of () = 0B 可以看到:() 是一个零大小类型(Zero-Sized Type,简称 ZST)。它在编译期是一个完整的类型实体,到了运行时则占 0 个字节,CPU 不会为它生成任何搬运数据的机器指令。
表达式 vs 语句:决定返回值的分号 ;
Section titled “表达式 vs 语句:决定返回值的分号 ;”再看 fn-and-unit.rs 里的 add 函数:它的最后一行只写了 a + b,既没写 return,末尾也没加分号 ;。
如果注释掉 a + b,取消第 6 行 a + b;(仅多加一个分号)的注释,编译器会立刻报错:
error[E0308]: mismatched types --> types/structs/src/bin/fn-and-unit.rs:4:27 |4 | fn add(a: i32, b: i32) -> i32 { | --- ^^^ expected `i32`, found `()`5 | // a + b6 | a + b; | - help: remove this semicolon to return this value为什么多加一个分号 ;,返回类型就从 i32 变成了单元类型 ()?这要从表达式(Expression)与语句(Statement)这两个基础概念说起。
用一句大白话讲:凡是能算出一个结果(值)的代码片段,就是「表达式」。比如最简单的 1 + 1 就是一个表达式,它算出来的结果是 2(因此可以直接放在等号右边写 let x = 1 + 1;);同理,一次函数调用 add(3, 4) 也是表达式,算出来的结果是 7。
而「语句」则是只执行一个动作、不向外产出结果的代码行(或者说它对外只产出空值 ())。
| 构件 | 大白话定义 | 末尾是否有分号 ; |
求值产出的类型 |
|---|---|---|---|
| 表达式(Expression) | 算完之后会留下一个值(如 1 + 1、a + b、add(3, 4)) |
不带分号 | 产出结果的具体类型 T(如 i32) |
| 语句(Statement) | 干完活就结束,把值消化在原地(如 let x = 1; 或在表达式后加 ;) |
带分号 ; |
丢弃内部算出的值,对外固定产出 () |
在任何花括号代码块 { ... } 的最后一行:
- 不加分号(尾部表达式
a + b):算出的值直接成为整个代码块(或函数)的返回值; - 加上分号(语句
a + b;):算出的i32被当场丢弃,代码块最终交出(),从而与签名-> i32冲突并报E0308。
(注:return 关键字仅用于需要从函数中途提前退出的场景,函数正常结尾直接写不带分号的表达式。)
控制流与代码块表达式
Section titled “控制流与代码块表达式”既然「凡是能算出一个值的代码片段就是表达式」,那么在传统语言里,只有 1 + 1、add(3, 4) 算表达式,而 if / else、循环和花括号代码块 { ... } 只管跳转执行,算不出一个值,所以不能写在 let x = ... 的等号右边。
但在 Rust 中,花括号代码块 { ... }、if / else 乃至 loop 循环,统统都能像 1 + 1 一样直接算出一个值——它们全都是表达式,可以直接赋给变量。
1. if / else if / else 表达式
Section titled “1. if / else if / else 表达式”Rust 的条件分支可以直接写在 let 的等号右边(替代其他语言里的三元运算符 cond ? a : b):
let grade = if score >= 90 { "A"} else if score >= 60 { "B"} else { "C"};写 if 时需要遵守三条类型约束:
- 条件不用圆括号包裹,且必须是
bool类型:写if score >= 90,不能写if score(Rust 不会把整数0或空内容自动当成false); - 分支末尾不加分号:每个分支最后一行不加分号以交出值(最外层末尾的
;用于结束整条let语句); - 所有分支必须产出同一个类型:如果一边返回
"pass"(&str),另一边返回0(整数),编译器会当场报error[E0308]。同理,如果一个if省略了else分支,它就只能返回单元类型()。
2. 代码块表达式 { ... }
Section titled “2. 代码块表达式 { ... }”任意一对花括号 { ... } 本身也是一个表达式:
let bonus = { let base = 10; base + 5 // 不加分号,作为整个花括号代码块的求值结果};花括号既构成了独立的局部变量作用域(base 在遇到 } 时销毁),同时又把最后一行不带分号的 base + 5(15)赋给了外面的 bonus。
3. 循环控制流:while 与 loop
Section titled “3. 循环控制流:while 与 loop”除后面第 9 章专门展开的 for 迭代器循环外,Rust 提供两种基础循环(均支持用 continue 跳入下一轮、用 break 退出循环):
// 1. while 条件循环(求值结果固定为 ())while count < 2 { count += 1;}
// 2. loop 无限循环表达式(支持用 break 交出计算结果)let code = loop { count += 1; if count == 3 { break count * 100; }};4. 运行完整实验核对
Section titled “4. 运行完整实验核对”运行下面这段包含上述所有控制流表达式的代码:
源文件:expr-blocks.rs
// Uncomment the marked `let bad = ...` block to trigger error[E0308].fn main() { let score = 85;
let grade = if score >= 90 { "A" } else if score >= 60 { "B" } else { "C" }; // let bad = if score >= 60 { "pass" } else { 0 };
let bonus = { let base = 10; base + 5 };
let mut count = 0; while count < 2 { count += 1; }
let code = loop { count += 1; if count == 3 { break count * 100; } };
println!("grade = {grade}, bonus = {bonus}, code = {code}");}运行输出:
运行输出grade = B, bonus = 15, code = 300如果取消第 12 行 // let bad = if score >= 60 { "pass" } else { 0 }; 的注释,编译器会立刻报出分支类型不匹配错误:
error[E0308]: `if` and `else` have incompatible types --> types/structs/src/bin/expr-blocks.rs:12:48 |12 | let bad = if score >= 60 { "pass" } else { 0 }; | ------ ^ expected `&str`, found integer | | | expected because of this- 函数签名与元组:函数参数与返回值类型必须显式写出;需要返回多个值时用元组
(T1, T2),无返回值的函数默认返回占 0 字节的单元类型(); - 分号
;决定表达式与语句:代码块末尾不带分号的表达式向外返回类型T的值;加上分号;则把值丢弃并返回(); - 控制流也是表达式:
if条件必须是bool;当把{ ... }、if / else或loop { break val; }用作表达式赋值时,所有分支必须产出同一个类型。
阅读下面这段代码,判断它能否通过编译?如果不能,指出哪一行违反了什么规则:
fn check_positive(n: i32) -> bool { if n > 0 { true; } else { false }}核对答案:不能通过编译(报 error[E0308]: mismatched types)。
第 3 行 true; 末尾多加了分号 ;,把表达式变成了语句,导致 if 分支丢弃了 true 并产出 (),与 else 分支的 bool 类型及函数签名 -> bool 冲突。去掉 true; 后的分号即可。
掌握了函数签名、元组、() 与表达式控制流,下一节我们正式打造自己的复合类型——看结构体(struct)如何把多个字段组织成一个新的所有权单元。