跳转到内容

5.1 函数、元组与表达式

在前两章中,我们已经确立了「一切皆类型」的视角。在动手定义结构体(struct)和枚举(enum)之前,我们先要把代码里最基础的组织骨架——函数、元组与控制流(if / else、while、loop)——一次性立起来。

顺着类型的视角看过去,你会发现 Rust 的函数与控制流比其他语言更加统一:没有 void 特例,而且连代码块 {}、if / else 和 loop 本身都有确定的类型。


先看 Rust 中最常见的三类函数返回形式:返回单个值、用元组返回多个值,以及不返回业务数据。

1. 函数签名:参数与返回类型不能省略

Section titled “1. 函数签名:参数与返回类型不能省略”

Rust 用 fn 关键字定义函数,函数名遵循蛇形命名法(snake_case):

fn add(a: i32, b: i32) -> i32 {
a + b
}
在 Rust Playground 中运行
  • 参数列表:写成 参数名: 类型,多个参数用逗号分隔(如 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) }
}
在 Rust Playground 中运行

元组长度在编译期固定;若内部元素都实现了 Copy,元组自身也自动实现 Copy。拿到元组后有两种取值方式:

let pair: (i32, i32) = min_max(9, 2);
let (low, high) = pair; // 1. 模式解构:拆成两个独立变量
let max_val = pair.1; // 2. 点号加下标:取第 1 个元素
在 Rust Playground 中运行

3. 单元类型 ():为什么 Rust 没有 void?

Section titled “3. 单元类型 ():为什么 Rust 没有 void?”

如果一个元组里一个元素都不放,写出来就是一对空括号 (),在 Rust 中称为单元类型(Unit Type),它唯一的值也写作 ()。

fn log_ready(tag: &str) {
println!("[{tag}] ready");
}
在 Rust Playground 中运行

在 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::<()>()
);
}
在 Rust Playground 中运行

运行输出:

运行输出
[init] ready
sum = 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;(仅多加一个分号)的注释,编译器会立刻报错:

编译器诊断摘要:在 add 函数末尾的 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 + b
6 | 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 关键字仅用于需要从函数中途提前退出的场景,函数正常结尾直接写不带分号的表达式。)


既然「凡是能算出一个值的代码片段就是表达式」,那么在传统语言里,只有 1 + 1、add(3, 4) 算表达式,而 if / else、循环和花括号代码块 { ... } 只管跳转执行,算不出一个值,所以不能写在 let x = ... 的等号右边。

但在 Rust 中,花括号代码块 { ... }、if / else 乃至 loop 循环,统统都能像 1 + 1 一样直接算出一个值——它们全都是表达式,可以直接赋给变量。

Rust 的条件分支可以直接写在 let 的等号右边(替代其他语言里的三元运算符 cond ? a : b):

let grade = if score >= 90 {
"A"
} else if score >= 60 {
"B"
} else {
"C"
};
在 Rust Playground 中运行

写 if 时需要遵守三条类型约束:

  1. 条件不用圆括号包裹,且必须是 bool 类型:写 if score >= 90,不能写 if score(Rust 不会把整数 0 或空内容自动当成 false);
  2. 分支末尾不加分号:每个分支最后一行不加分号以交出值(最外层末尾的 ; 用于结束整条 let 语句);
  3. 所有分支必须产出同一个类型:如果一边返回 "pass"(&str),另一边返回 0(整数),编译器会当场报 error[E0308]。同理,如果一个 if 省略了 else 分支,它就只能返回单元类型 ()。

任意一对花括号 { ... } 本身也是一个表达式:

let bonus = {
let base = 10;
base + 5 // 不加分号,作为整个花括号代码块的求值结果
};
在 Rust Playground 中运行

花括号既构成了独立的局部变量作用域(base 在遇到 } 时销毁),同时又把最后一行不带分号的 base + 5(15)赋给了外面的 bonus。

除后面第 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;
}
};
在 Rust Playground 中运行

运行下面这段包含上述所有控制流表达式的代码:

源文件: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}");
}
在 Rust Playground 中运行

运行输出:

运行输出
grade = B, bonus = 15, code = 300

如果取消第 12 行 // let bad = if score >= 60 { "pass" } else { 0 }; 的注释,编译器会立刻报出分支类型不匹配错误:

编译器诊断摘要:取消 expr-blocks.rs 第 12 行注释后
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

  1. 函数签名与元组:函数参数与返回值类型必须显式写出;需要返回多个值时用元组 (T1, T2),无返回值的函数默认返回占 0 字节的单元类型 ();
  2. 分号 ; 决定表达式与语句:代码块末尾不带分号的表达式向外返回类型 T 的值;加上分号 ; 则把值丢弃并返回 ();
  3. 控制流也是表达式:if 条件必须是 bool;当把 { ... }、if / else 或 loop { break val; } 用作表达式赋值时,所有分支必须产出同一个类型。

阅读下面这段代码,判断它能否通过编译?如果不能,指出哪一行违反了什么规则:

fn check_positive(n: i32) -> bool {
if n > 0 {
true;
} else {
false
}
}
在 Rust Playground 中运行

核对答案:不能通过编译(报 error[E0308]: mismatched types)。 第 3 行 true; 末尾多加了分号 ;,把表达式变成了语句,导致 if 分支丢弃了 true 并产出 (),与 else 分支的 bool 类型及函数签名 -> bool 冲突。去掉 true; 后的分号即可。


掌握了函数签名、元组、() 与表达式控制流,下一节我们正式打造自己的复合类型——看结构体(struct)如何把多个字段组织成一个新的所有权单元。

继续:结构体与部分移动 →