《Rust 编程实战》系列第 9 篇
在前面的文章中,我们已经学习了变量、数据类型、常量和静态变量。
接下来,我们需要解决一个非常重要的问题:
如何把一段功能独立出来,并在程序中的多个地方重复使用?
答案就是:函数(Function)。
函数是组织 Rust 程序最基本的方式之一。无论是命令行工具、Web 服务、桌面软件还是企业级项目,最终都会由大量函数共同组成。
本文将详细介绍:
- 如何定义函数
- 如何传递参数
- 如何声明参数类型
- 如何返回数据
- Rust 中语句和表达式的区别
return与隐式返回的区别- 单元类型
() - 函数的常见错误和最佳实践
什么是函数?
函数可以理解为一段完成特定任务的代码。
例如:
1fn say_hello() { 2 println!("Hello, Rust!"); 3} 4
这里定义了一个名为:
1say_hello 2
的函数。
它的功能是输出:
1Hello, Rust! 2
函数定义后不会自动执行,必须调用它。
完整代码如下:
1fn main() { 2 say_hello(); 3} 4 5fn say_hello() { 6 println!("Hello, Rust!"); 7} 8
运行结果:
1Hello, Rust! 2
fn 关键字
Rust 使用:
1fn 2
定义函数。
基本语法如下:
1fn 函数名() { 2 // 函数体 3} 4
例如:
1fn print_message() { 2 println!("正在学习 Rust"); 3} 4
调用函数:
1print_message(); 2
函数名通常使用蛇形命名法,也就是:
1snake_case 2
例如:
1fn get_user_name() {} 2 3fn calculate_total_price() {} 4 5fn save_order_data() {} 6
不推荐:
1fn GetUserName() {} 2
main 函数
每个 Rust 可执行程序都需要一个入口函数:
1fn main() { 2} 3
程序启动时,会首先执行 main 函数。
例如:
1fn main() { 2 println!("程序开始运行"); 3} 4
对于普通可执行项目来说,main 函数是必须存在的。
不过,库项目通常使用:
1src/lib.rs 2
而不需要 main 函数。
函数可以定义在 main 后面吗?
可以。
例如:
1fn main() { 2 say_hello(); 3} 4 5fn say_hello() { 6 println!("Hello"); 7} 8
也可以写在 main 前面:
1fn say_hello() { 2 println!("Hello"); 3} 4 5fn main() { 6 say_hello(); 7} 8
Rust 不要求函数必须先定义再调用。
只要函数位于当前模块中,编译器就可以找到它。
函数参数
函数可以接收外部传入的数据,这些数据称为参数。
例如:
1fn greet(name: &str) { 2 println!("你好,{}!", name); 3} 4
调用:
1fn main() { 2 greet("Tom"); 3 greet("Alice"); 4} 5
输出:
1你好,Tom! 2你好,Alice! 3
这里:
1name: &str 2
表示函数接收一个名为 name 的参数,类型是 &str。
Rust 函数参数必须指定类型
Rust 不允许省略函数参数类型。
错误写法:
1fn greet(name) { 2 println!("{}", name); 3} 4
编译器无法知道 name 的类型。
正确写法:
1fn greet(name: &str) { 2 println!("{}", name); 3} 4
这是 Rust 与部分动态语言不同的地方。
变量可以通过初始值推导类型:
1let age = 18; 2
但函数参数必须明确写出类型:
1fn show_age(age: i32) { 2 println!("{}", age); 3} 4
多个函数参数
函数可以接收多个参数。
例如:
1fn print_user(name: &str, age: u8) { 2 println!("姓名:{},年龄:{}", name, age); 3} 4
调用:
1fn main() { 2 print_user("Tom", 20); 3} 4
输出:
1姓名:Tom,年龄:20 2
每个参数都必须指定类型:
1fn print_user(name: &str, age: u8) 2
参数之间使用逗号分隔。
参数顺序必须一致
例如:
1fn print_user(name: &str, age: u8) { 2 println!("{} {}", name, age); 3} 4
正确调用:
1print_user("Alice", 18); 2
错误调用:
1print_user(18, "Alice"); 2
因为参数位置和类型不匹配。
Rust 会在编译阶段报错,而不是等程序运行时才发现问题。
函数返回值
函数除了执行操作,也可以返回结果。
例如:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
这里:
1-> i32 2
表示函数返回一个 i32 类型的值。
调用:
1fn main() { 2 let result = add(10, 20); 3 4 println!("{}", result); 5} 6
输出:
130 2
返回值语法
Rust 函数返回值的语法如下:
1fn 函数名(参数) -> 返回类型 { 2 返回值 3} 4
例如:
1fn get_age() -> u8 { 2 18 3} 4
调用:
1fn main() { 2 let age = get_age(); 3 4 println!("{}", age); 5} 6
输出:
118 2
为什么返回值后面没有分号?
这是 Rust 初学者最容易困惑的地方。
正确写法:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
错误写法:
1fn add(a: i32, b: i32) -> i32 { 2 a + b; 3} 4
区别就在于最后的分号。
在 Rust 中:
1a + b 2
是表达式,会产生一个值。
而:
1a + b; 2
加上分号后,就变成了语句,不再返回这个值。
语句与表达式
理解函数返回值,必须理解 Rust 中的两个概念:
- 语句(Statement)
- 表达式(Expression)
语句
语句执行操作,但通常不返回有意义的值。
例如:
1let x = 10; 2
这是一个语句。
再例如:
1println!("Hello"); 2
也是一个语句。
表达式
表达式会计算并产生一个值。
例如:
110 + 20 2
结果是:
130 2
再例如:
1{ 2 let x = 10; 3 x + 1 4} 5
整个代码块的值是:
111 2
代码块也可以返回值
Rust 中的大括号代码块本身也是表达式。
例如:
1fn main() { 2 let result = { 3 let a = 10; 4 let b = 20; 5 6 a + b 7 }; 8 9 println!("{}", result); 10} 11
输出:
130 2
这里代码块最后一行:
1a + b 2
没有分号,因此这个值成为整个代码块的结果。
使用 return 返回值
Rust 也支持 return 关键字。
例如:
1fn add(a: i32, b: i32) -> i32 { 2 return a + b; 3} 4
注意:
使用 return 时,后面通常需要分号:
1return a + b; 2
这与隐式返回不同。
隐式返回:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
显式返回:
1fn add(a: i32, b: i32) -> i32 { 2 return a + b; 3} 4
两种写法结果相同。
什么时候使用 return?
如果返回值位于函数最后一行,通常推荐使用隐式返回:
1fn multiply(a: i32, b: i32) -> i32 { 2 a * b 3} 4
如果需要提前结束函数,则使用 return。
例如:
1fn get_level(score: i32) -> &'static str { 2 if score < 0 { 3 return "分数无效"; 4 } 5 6 if score >= 90 { 7 return "优秀"; 8 } 9 10 if score >= 60 { 11 return "及格"; 12 } 13 14 "不及格" 15} 16
这里多个分支都可能提前返回。
if 也可以作为表达式
Rust 中的 if 不只是控制语句,也可以返回值。
例如:
1fn get_status(age: u8) -> &'static str { 2 if age >= 18 { 3 "成年人" 4 } else { 5 "未成年人" 6 } 7} 8
调用:
1fn main() { 2 let status = get_status(20); 3 4 println!("{}", status); 5} 6
输出:
1成年人 2
还可以直接赋值:
1fn main() { 2 let age = 20; 3 4 let status = if age >= 18 { 5 "成年人" 6 } else { 7 "未成年人" 8 }; 9 10 println!("{}", status); 11} 12
if 各分支类型必须一致
例如:
1let value = if true { 2 10 3} else { 4 20 5}; 6
这是合法的,因为两个分支都是整数。
下面的代码会报错:
1let value = if true { 2 10 3} else { 4 "hello" 5}; 6
因为一个分支返回整数,另一个分支返回字符串。
Rust 无法为变量确定统一类型。
没有返回值的函数
有些函数只执行操作,不需要返回数据。
例如:
1fn print_message() { 2 println!("Hello"); 3} 4
这类函数实际上也有返回类型,只是返回的是:
1() 2
它称为:
单元类型(Unit Type)。
上面的函数也可以写成:
1fn print_message() -> () { 2 println!("Hello"); 3} 4
一般情况下不需要显式写出来。
单元类型 ()
() 表示没有实际数据的返回值。
例如:
1fn do_nothing() -> () { 2} 3
也可以:
1fn do_nothing() { 2} 3
两者等价。
单元类型在 Rust 中经常出现,例如:
- 普通打印函数
- 修改外部状态的函数
- 没有返回数据的回调
Result<(), E>中表示成功但不返回内容
例如:
1fn save_data() -> Result<(), String> { 2 println!("保存成功"); 3 4 Ok(()) 5} 6
这里:
1Ok(()) 2
表示操作成功,但没有额外数据需要返回。
返回多个值
Rust 函数只能返回一个值,但可以借助元组返回多个数据。
例如:
1fn get_user() -> (&'static str, u8, bool) { 2 ("Tom", 20, true) 3} 4
调用:
1fn main() { 2 let user = get_user(); 3 4 println!("姓名:{}", user.0); 5 println!("年龄:{}", user.1); 6 println!("状态:{}", user.2); 7} 8
也可以解构:
1fn main() { 2 let (name, age, active) = get_user(); 3 4 println!("{} {} {}", name, age, active); 5} 6
实战:计算商品总价
下面写一个简单的函数,计算商品总价。
1fn calculate_total(price: f64, quantity: u32) -> f64 { 2 price * quantity as f64 3} 4 5fn main() { 6 let total = calculate_total(29.9, 3); 7 8 println!("商品总价:{:.2}", total); 9} 10
输出:
1商品总价:89.70 2
这里:
1quantity as f64 2
把 u32 转换为 f64,这样才能与价格相乘。
实战:计算折扣价格
1fn calculate_discount_price(price: f64, discount: f64) -> f64 { 2 if discount <= 0.0 || discount > 1.0 { 3 return price; 4 } 5 6 price * discount 7} 8 9fn main() { 10 let final_price = calculate_discount_price(100.0, 0.8); 11 12 println!("折后价格:{:.2}", final_price); 13} 14
输出:
1折后价格:80.00 2
当折扣值无效时,函数会提前返回原价。
实战:判断数字是否为偶数
1fn is_even(number: i32) -> bool { 2 number % 2 == 0 3} 4 5fn main() { 6 let result = is_even(10); 7 8 println!("是否为偶数:{}", result); 9} 10
输出:
1是否为偶数:true 2
这里:
1number % 2 == 0 2
本身就是一个布尔表达式,因此可以直接作为返回值。
不需要写成:
1if number % 2 == 0 { 2 true 3} else { 4 false 5} 6
前一种写法更简洁。
函数参数与变量可变性
函数参数默认也是不可变的。
例如:
1fn increase(number: i32) { 2 number += 1; 3} 4
会报错,因为 number 不可变。
可以改成:
1fn increase(mut number: i32) { 2 number += 1; 3 4 println!("{}", number); 5} 6
这里的 mut 只表示函数内部的参数变量可以修改。
完整示例:
1fn increase(mut number: i32) { 2 number += 1; 3 4 println!("函数内部:{}", number); 5} 6 7fn main() { 8 let number = 10; 9 10 increase(number); 11 12 println!("函数外部:{}", number); 13} 14
输出:
1函数内部:11 2函数外部:10 3
为什么外部还是 10?
因为整数类型默认会复制一份传入函数。函数内部修改的是参数副本,不是外部变量。
这个问题与 Rust 的所有权和 Copy 类型有关,后续会详细学习。
函数调用必须匹配返回类型
例如:
1fn get_age() -> i32 { 2 "18" 3} 4
会报错。
因为函数声明返回:
1i32 2
实际却返回:
1&str 2
正确写法:
1fn get_age() -> i32 { 2 18 3} 4
Rust 会在编译阶段检查返回值类型。
常见错误
参数没有指定类型
错误:
1fn add(a, b) { 2 println!("{}", a + b); 3} 4
正确:
1fn add(a: i32, b: i32) { 2 println!("{}", a + b); 3} 4
返回表达式后加了分号
错误:
1fn add(a: i32, b: i32) -> i32 { 2 a + b; 3} 4
正确:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
或者:
1fn add(a: i32, b: i32) -> i32 { 2 return a + b; 3} 4
返回值类型不一致
错误:
1fn get_value(ok: bool) -> i32 { 2 if ok { 3 10 4 } else { 5 "error" 6 } 7} 8
两个分支必须返回相同类型。
正确:
1fn get_value(ok: bool) -> i32 { 2 if ok { 3 10 4 } else { 5 0 6 } 7} 8
调用函数时参数数量不一致
函数定义:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
错误调用:
1add(10); 2
或者:
1add(10, 20, 30); 2
参数数量必须完全一致。
把打印结果当作返回值
例如:
1fn add(a: i32, b: i32) -> i32 { 2 println!("{}", a + b); 3} 4
println! 的返回值是 (),不是 i32。
正确写法:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
打印应该放在调用位置:
1fn main() { 2 let result = add(10, 20); 3 4 println!("{}", result); 5} 6
函数设计最佳实践
一个函数只做一件事
不推荐把登录、保存数据、发送邮件和写日志全部放在一个函数里。
推荐拆分:
1fn validate_user() {} 2 3fn save_user() {} 4 5fn send_email() {} 6 7fn write_log() {} 8
这样代码更容易测试和维护。
函数名要表达意图
不推荐:
1fn do_it() {} 2
推荐:
1fn calculate_order_total() {} 2 3fn find_user_by_id() {} 4 5fn create_access_token() {} 6
看到函数名就能理解它的用途。
参数不要过多
如果一个函数有大量参数:
1fn create_user( 2 name: String, 3 age: u8, 4 email: String, 5 phone: String, 6 city: String, 7 address: String, 8) { 9} 10
后续可以考虑使用结构体:
1struct CreateUserRequest { 2 name: String, 3 age: u8, 4 email: String, 5 phone: String, 6 city: String, 7 address: String, 8} 9
然后函数只接收一个参数:
1fn create_user(request: CreateUserRequest) { 2} 3
优先返回结果,而不是在函数内部直接打印
不推荐:
1fn add(a: i32, b: i32) { 2 println!("{}", a + b); 3} 4
更推荐:
1fn add(a: i32, b: i32) -> i32 { 2 a + b 3} 4
这样调用方可以决定如何使用结果:
1let result = add(10, 20); 2 3println!("{}", result); 4
也可以继续参与计算:
1let total = add(10, 20) * 2; 2
完整练习
下面是一个简单的订单金额计算程序。
1fn calculate_subtotal(price: f64, quantity: u32) -> f64 { 2 price * quantity as f64 3} 4 5fn calculate_discount(subtotal: f64, discount_rate: f64) -> f64 { 6 if discount_rate <= 0.0 || discount_rate > 1.0 { 7 return subtotal; 8 } 9 10 subtotal * discount_rate 11} 12 13fn calculate_shipping(final_price: f64) -> f64 { 14 if final_price >= 99.0 { 15 0.0 16 } else { 17 10.0 18 } 19} 20 21fn main() { 22 let price = 39.9; 23 let quantity = 3; 24 let discount_rate = 0.9; 25 26 let subtotal = calculate_subtotal(price, quantity); 27 let discounted_price = calculate_discount(subtotal, discount_rate); 28 let shipping = calculate_shipping(discounted_price); 29 let total = discounted_price + shipping; 30 31 println!("商品小计:{:.2}", subtotal); 32 println!("折后金额:{:.2}", discounted_price); 33 println!("运费:{:.2}", shipping); 34 println!("订单总额:{:.2}", total); 35} 36
可能输出:
1商品小计:119.70 2折后金额:107.73 3运费:0.00 4订单总额:107.73 5
这个示例将不同功能拆分成多个函数:
calculate_subtotal:计算商品小计calculate_discount:计算折扣金额calculate_shipping:计算运费
这就是函数最重要的价值:拆分复杂逻辑,让代码更容易理解和复用。
本章小结
函数是 Rust 程序组织代码的基础。
通过本文,我们学习了:
- 使用
fn定义函数 - 函数命名规范
- 函数参数和参数类型
- 使用
->声明返回类型 - 隐式返回和
return - 语句与表达式的区别
- 为什么返回表达式不能加分号
- 单元类型
() - 使用元组返回多个值
if表达式返回值- 函数参数的可变性
- 常见错误与函数设计实践
其中最重要的一点是:
Rust 函数体最后一个不带分号的表达式,会成为函数的返回值。
理解表达式和返回值之后,后面学习控制流、闭包、错误处理和所有权都会更加轻松。
下一篇预告
下一篇我们将学习 Rust 注释与文档注释,包括:
- 普通单行注释
- 多行注释
- 文档注释
/// - 模块文档注释
//! - Markdown 文档
- 文档代码示例
- 使用
cargo doc生成项目文档 - 如何编写高质量的 Rust API 文档
《Rust 函数与返回值详解:参数、表达式与返回类型》 是转载文章,点击查看原文。
