C 程序员看 Rust 函数,第一眼会觉得只是语法换了位置:
int add(int a, int b)
{
return a + b;
}
Rust 写成:
fn add(a: i32, b: i32) -> i32 {
a + b
}
这两个函数做的是同一件事,但 Rust 函数有几个早期就要适应的习惯:
- 参数名在前,类型在后
- 返回类型写在
->后 - 函数末尾表达式可以作为返回值
return更多用于早返回- 没有返回值的函数返回
() - C 的输出参数和返回码,在 Rust 里通常会变成返回值和
Result
这些都不是所有权的完整理论,但会直接影响你怎么写第一个库函数。
参数名在前,类型在后
C 的函数参数是类型在前:
int checksum(const uint8_t *data, size_t len);
Rust 是名字在前、类型在后:
fn checksum(data: &[u8]) -> u32 {
data.iter().map(|b| *b as u32).sum()
}
这里先不用展开 slice 的完整规则,只抓住第一层对应关系:
const uint8_t *data, size_t len -> data: &[u8]
C 里靠两个参数表达“指针加长度”。Rust 里用一个 slice 参数表达“这是一段连续数据的只读视图”。
如果函数只需要一个整数,写法也很直接:
fn retry_delay_ms(retry: u32) -> u32 {
retry * 100
}
参数类型是函数契约的一部分。Rust 不会像 C 一样靠隐式转换帮你把各种整数混过去。
返回类型写在箭头后面
C 的返回类型写在函数名前:
uint32_t checksum(const uint8_t *data, size_t len);
Rust 的返回类型写在参数列表后:
fn checksum(data: &[u8]) -> u32 {
data.iter().map(|b| *b as u32).sum()
}
没有返回值时,Rust 可以省略 -> ():
fn log_packet(len: usize) {
eprintln!("packet len={len}");
}
完整写出来是:
fn log_packet(len: usize) -> () {
eprintln!("packet len={len}");
}
() 可以先理解成“没有有用返回值”。它不是 C 的 void 完全等价物,但在第一层函数使用里,基本承担同一个位置。
末尾表达式就是返回值
C 里通常显式 return:
int add(int a, int b)
{
return a + b;
}
Rust 可以写:
fn add(a: i32, b: i32) -> i32 {
a + b
}
末尾没有分号的表达式就是返回值。
如果加了分号,就变成语句,返回 ():
fn add(a: i32, b: i32) -> i32 {
a + b; // 编译错误:这里实际返回 ()
}
这条规则和前面讲过的“代码块可以产生值”是一致的。函数体也是一个块,块的末尾表达式可以成为函数返回值。
return 常用于早返回
Rust 也有 return:
fn first_byte(data: &[u8]) -> Option<u8> {
if data.is_empty() {
return None;
}
Some(data[0])
}
这里 return 用在错误或特殊路径,主路径留在函数末尾。
C 里也常这么写:
int first_byte(const uint8_t *data, size_t len, uint8_t *out)
{
if (len == 0 || out == NULL) {
return -1;
}
*out = data[0];
return 0;
}
Rust 的差异在于:如果“没有值”是合法结果,可以直接返回 Option:
fn first_byte(data: &[u8]) -> Option<u8> {
data.first().copied()
}
输出参数和返回码不一定要保留成原来的形状。
输出参数通常变成返回值
C 里为了返回多个东西,常用输出参数:
int parse_header(const uint8_t *data, size_t len, struct Header *out);
Rust 里更常见的是直接返回结果:
fn parse_header(data: &[u8]) -> Result<Header, ParseError> {
if data.len() < 4 {
return Err(ParseError::TooShort);
}
Ok(Header {
version: data[0],
length: u16::from_be_bytes([data[1], data[2]]),
})
}
对应关系可以先这样看:
C 返回码 + out 参数 -> Rust Result<返回值, 错误>
这不只是语法变化。Rust 调用方拿到的类型会提醒他:这里可能失败,成功时才有 Header。
如果函数确实要写入调用方提供的 buffer,才使用可变参数:
fn fill_header(out: &mut [u8]) -> Result<usize, EncodeError> {
if out.len() < 4 {
return Err(EncodeError::TooSmall);
}
out[0] = 1;
out[1] = 0;
out[2] = 4;
out[3] = 0;
Ok(4)
}
这对应 C 里的 uint8_t *out, size_t capacity,但 Rust 类型把“可写 slice”表达出来了。
函数参数默认按值传递
C 的参数也是按值传递:
void set_retry(int retry)
{
retry = 0;
}
修改 retry 不会改调用方变量。
Rust 也一样:
fn set_retry(mut retry: i32) {
retry = 0;
}
这里的 mut retry 只表示函数内部这个参数绑定可以改,不表示调用方的变量会被改。
如果要修改调用方提供的数据,C 用指针:
void reset_retry(int *retry)
{
*retry = 0;
}
Rust 第一层对应是可变引用:
fn reset_retry(retry: &mut i32) {
*retry = 0;
}
调用时也要显式借出可变引用:
let mut retry = 3;
reset_retry(&mut retry);
这部分后面会单独讲借用。现在先记住:参数绑定上的 mut 和 &mut 不是一回事。
mut retry: i32 -> 函数内部可以改这个局部参数
retry: &mut i32 -> 函数可以改调用方传进来的那个值
函数可以放在后面,但模块边界要另讲
C 调用函数前通常需要声明:
int add(int a, int b);
int main(void)
{
return add(1, 2);
}
Rust 在同一个模块里不需要提前声明:
fn main() {
println!("{}", add(1, 2));
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
但这不等于 Rust 没有接口边界。跨文件、跨模块、跨 crate 时,会涉及 mod、pub、use 和 package 组织。那是模块和文件组织的问题,不要和单个函数语法混在一起。
最小迁移表
C int add(int a, int b) -> Rust fn add(a: i32, b: i32) -> i32
C void log(int x) -> Rust fn log(x: i32)
C return expr -> Rust expr 或 return expr
C 末尾 return -> Rust 末尾表达式
C 返回码 + out 参数 -> Rust Result<T, E>
C T *out -> Rust &mut T / &mut [T]
C const uint8_t *p, size_t n -> Rust &[u8]
C 参数内部可改 -> Rust mut 参数绑定
C 修改调用方变量 -> Rust &mut 参数
C 函数提前声明 -> Rust 同模块内不需要原型声明
Rust 函数的第一层并不复杂。真正重要的是:函数签名会更明确地表达数据是输入、输出、可变输出,还是可能失败的结果。C 里靠命名和注释维护的约定,Rust 会尽量放进类型里。