C 程序员看 Rust 运算符,最容易误判的不是 +、-、*、/ 长得像不像,而是 C 里很多自动发生的转换,在 Rust 里不会自动发生。
C 代码里常见这种写法:
#include <stdint.h>
#include <stddef.h>
uint8_t version = 1;
size_t len = 128;
uint32_t total = version + len;
if (total) {
/* ... */
}
Rust 会要求你把类型关系写清楚:
let version: u8 = 1;
let len: usize = 128;
let total: usize = version as usize + len;
if total != 0 {
// ...
}
从这段代码开始,最先要调整的是类型变化必须显式出现。控制流怎么组织,可以在这个基础上继续往后看。
算术运算要求类型一致
C 会做整数提升和 usual arithmetic conversions:
uint8_t a = 10;
uint32_t b = 20;
uint32_t c = a + b;
a 会参与提升,表达式可以通过。
Rust 不会自动把 u8 变成 u32:
let a: u8 = 10;
let b: u32 = 20;
let c = a + b; // 编译错误
要写清楚转换:
let c = a as u32 + b;
这让代码多了几个字符,但少了很多隐藏规则。尤其在协议、文件格式、长度计算里,隐式转换很容易把有符号、无符号、平台相关宽度混在一起。
usize 不要随手和字段宽度混算
C 里 size_t 和 uint32_t 混用很常见:
uint32_t packet_len = header.length;
size_t offset = 4;
size_t end = offset + packet_len;
Rust 会拦住这种混算:
let packet_len: u32 = 128;
let offset: usize = 4;
let end = offset + packet_len; // 编译错误
你要先决定语义:这是内存下标,还是外部格式字段。
如果要拿来切 slice,转换到 usize:
let end = offset + packet_len as usize;
如果要写回协议字段,保持固定宽度:
let packet_len: u32 = payload.len() as u32;
后一种写法还要考虑长度是否可能超过 u32::MAX。更稳的库代码通常用 try_from:
let packet_len = u32::try_from(payload.len()).map_err(|_| EncodeError::TooLarge)?;
as 是转换,不是边界检查。会不会截断,要由你自己判断。
bool 不能由整数隐式得到
C 里条件接受整数:
if (flags & 0x01) {
enable();
}
Rust 里 if 条件必须是 bool:
let flags: u32 = 0x01;
if flags & 0x01 {
// 编译错误
}
要写成比较:
if flags & 0x01 != 0 {
enable();
}
位运算本身还是 &、|、^、!、<<、>>,但位运算结果是整数,不是条件。
这个差异会影响很多 C 风格判断:
if (len) -> if len != 0
if (ptr) -> if !ptr.is_null() // 裸指针场景
if (flags&m) -> if flags & m != 0
Rust 要你说清楚判断标准。
逻辑运算只处理 bool
C 里 && 和 || 可以处理整数条件:
if (len && enabled) {
/* ... */
}
Rust 的 && 和 || 两边必须是 bool:
let len: usize = 10;
let enabled = true;
if len != 0 && enabled {
// ...
}
短路规则仍然存在。右侧表达式只在需要时求值:
if !data.is_empty() && data[0] == 0x7e {
// ...
}
这里 data[0] 不会在空 slice 时执行。
赋值不是一个可拿来链式使用的值
C 里赋值表达式有值:
int a;
int b;
a = b = 0;
if ((n = read(fd, buf, sizeof buf)) > 0) {
/* ... */
}
Rust 里赋值表达式的值是 (),不适合写成 C 的链式赋值或条件赋值:
let mut a = 1;
let mut b = 2;
a = b = 0; // 编译错误
Rust 通常拆开写:
b = 0;
a = b;
读取结果也会单独绑定:
let n = reader.read(&mut buf)?;
if n > 0 {
// ...
}
这类写法少了 C 的紧凑,但错误路径和中间值更清楚。
代码块可以产生值
C 里的 { ... } 主要是语句块和作用域。Rust 的代码块也是表达式,可以产生值:
let size = {
let header = 4;
let payload = 128;
header + payload
};
块里最后一行没有分号,就是这个块的值。
这会影响很多 Rust 写法。比如 if 也可以产生值:
let limit = if debug { 64 } else { 1024 };
但两个分支类型必须一致:
let limit = if debug { 64 } else { "large" }; // 编译错误
C 程序员可以把它理解成:Rust 里不少控制结构既能控制流程,也能返回一个值,但类型必须在编译期对齐。
sizeof 对应 size_of
C 里用 sizeof:
size_t n = sizeof(uint32_t);
Rust 里用标准库函数:
use std::mem::size_of;
let n = size_of::<u32>();
size_of::<T>() 返回 usize。这适合检查内存里的类型大小,不适合直接决定外部协议布局。外部格式仍然应该自己规定字段宽度和字节序。
as 要谨慎使用
Rust 的 as 很方便:
let a: u32 = 300;
let b: u8 = a as u8;
但这可能截断。300_u32 as u8 不会变成 300,它会按目标类型规则转换。
边界可能出错时,优先用 try_from:
let b = u8::try_from(a).map_err(|_| Error::OutOfRange)?;
可以把规则简单分成两类:
明确知道不会越界的内部计算 -> as 可以接受
外部输入、文件字段、协议长度 -> 优先 try_from
Rust 不替你做隐式转换,但显式转换以后,责任就回到代码上。
最小迁移表
C a + b 自动提升 -> Rust 先统一类型
C size_t + uint32_t -> Rust 明确转成 usize 或固定宽度
C if (x) -> Rust if x != 0 / is_null / is_empty
C flags & mask -> Rust flags & mask != 0 才是条件
C a = b = 0 -> Rust 分开赋值
C sizeof(T) -> Rust size_of::<T>()
C 隐式数值转换 -> Rust as / try_from
C 语句块 -> Rust 块也可以是表达式
最重要的习惯变化是:C 把很多转换藏在表达式求值里,Rust 要你把类型变化写出来。写库和工具时,这反而能让长度、字段宽度、下标和错误路径更容易审查。