C 程序员进入 Rust,最先要过的不是所有权,而是能不能顺手写出普通变量、常量和基本类型。
C 里常见代码大概是这样:
#include <stdint.h>
#include <stdbool.h>
#define MAX_PACKET_SIZE 1024
int retry = 3;
uint32_t flags = 0;
bool enabled = true;
char tag = 'A';
Rust 也能表达这些东西,但默认习惯不完全一样:
const MAX_PACKET_SIZE: usize = 1024;
let mut retry: i32 = 3;
let flags: u32 = 0;
let enabled: bool = true;
let tag: u8 = b'A';
结构体布局、数组、指针和字符串都建立在这些基础类型之上。变量、常量和宽度先分清,后面的内存和 API 问题才不会混在一起。
变量默认不能改
C 里局部变量默认可修改:
int retry = 3;
retry = retry - 1;
Rust 里 let 绑定默认不可变:
let retry = 3;
retry = retry - 1; // 编译错误
需要修改时要写 mut:
let mut retry = 3;
retry = retry - 1;
这不是语法洁癖。C 代码里一个变量是否会被后续修改,通常要靠人读完整个作用域判断。Rust 把这个意图写在绑定处:默认不改,要改就显式 mut。
这会影响库代码的可读性。看到:
let len = data.len();
就知道 len 后面不会被重新赋值。看到:
let mut offset = 0;
就知道它是一个会推进的游标或累加状态。
类型可以标,也可以推断
C 声明变量时必须写类型:
int count = 0;
uint32_t flags = 0;
Rust 可以显式写:
let count: i32 = 0;
let flags: u32 = 0;
也可以让编译器从上下文推断:
let count = 0;
let flags = 0_u32;
没有上下文时,整数默认通常会落到 i32。写库、协议字段、文件格式时,不要依赖读者猜默认类型,关键位置应该把类型写清楚。
例如协议版本字段是 1 字节:
let version: u8 = 1;
长度来自 slice:
let len: usize = data.len();
数据格式字段和内存长度不是同一种语义。前者应该用明确宽度,后者通常用 usize。
整数优先写清宽度
C 里有两套整数习惯。
一套是传统类型:
int count;
long total;
unsigned flags;
另一套是 stdint.h:
uint8_t version;
int32_t temperature;
uint64_t timestamp;
Rust 的整数类型直接带符号和宽度:
uint8_t -> u8
uint16_t -> u16
uint32_t -> u32
uint64_t -> u64
int8_t -> i8
int16_t -> i16
int32_t -> i32
int64_t -> i64
写二进制协议、文件格式、寄存器字段、跨语言结构体时,优先用固定宽度整数。
usize 和 isize 更像 C 里的 size_t 和 ssize_t 这类平台相关大小:
let index: usize = 0;
let len: usize = data.len();
它们适合内存里的长度、下标和对象大小,不适合直接作为网络协议或文件格式字段。
bool 不是整数
C 在很长时间里没有内建 bool,后来通过 <stdbool.h> 使用 _Bool:
#include <stdbool.h>
bool enabled = true;
if (enabled) {
/* ... */
}
C 里很多代码还会把整数当条件:
if (flags) {
/* non-zero means true */
}
Rust 的 bool 是独立类型,条件表达式必须是 bool:
let enabled: bool = true;
if enabled {
// ...
}
整数不能直接当条件:
let flags: u32 = 1;
if flags != 0 {
// ...
}
这会让一些 C 写法变啰嗦,但意图更明确。if ptr、if len、if flags 这种压缩写法,在 Rust 里要写成具体判断。
Rust 的 char 不是 C 的 char
C 的 char 通常是一字节整数类型:
char tag = 'A';
Rust 的 char 表示一个 Unicode scalar value,大小是 4 字节:
let ch: char = '中';
如果你要表达一个字节,用 u8:
let tag: u8 = b'A';
如果你要表达一段文本,用 &str 或 String。如果你要表达 C 字符串,用 CStr 或 CString。不要把 Rust 的 char 当成 C 的 char 直接替换。
第一层判断可以这样记:
C char -> 常常是一个字节
Rust u8 -> 一个字节
Rust char -> 一个 Unicode 标量值
Rust &str/String -> UTF-8 文本
const 不是 C 的 const
C 里常见两种“常量”写法。
一种是宏:
#define MAX_PACKET_SIZE 1024
另一种是 const 限定对象:
const int max_packet_size = 1024;
Rust 的 const 不等于 C 的 const。它更像“有类型的编译期常量值”:
const MAX_PACKET_SIZE: usize = 1024;
Rust const 有类型,不是文本替换,也不是一个有固定唯一地址的只读对象。它适合表达数组容量、默认上限、协议常量、状态值等。
如果需要一个有固定存储位置的全局值,用 static:
static APP_NAME: &str = "iot-tool";
第一层先记住区别:
const -> 编译期常量值
static -> 有固定存储位置的全局值
对应关系不要写成 C const -> Rust const。更稳的是:
C #define 数值常量 -> Rust const
C enum 常量 -> Rust const / Rust enum
C const 局部只读变量 -> Rust let
C static const 全局对象 -> Rust const 或 static,看是否需要固定地址
大多数从 #define 迁移过来的数值常量,先考虑 Rust const。
shadowing 不是重新赋值
C 里同一个作用域不能重复定义同名变量:
int len = 10;
int len = len + 1; // 错误
Rust 允许 shadowing:
let len = "1024";
let len: usize = len.parse().unwrap();
第二个 len 不是修改第一个 len,而是创建了一个新的绑定,遮蔽了旧绑定。
这在处理输入时很常见:
let port = "1883";
let port: u16 = port.parse().unwrap();
变量名表达的是同一个业务概念,类型从文本变成了数字。不要把 shadowing 当成 C 的重新赋值,它更像“同名的新变量”。
最小迁移表
核心对应关系如下:
C int -> Rust i32 或按需求选择 i16/i64
C unsigned int -> Rust u32 或按需求选择 u16/u64
C uint8_t -> Rust u8
C size_t -> Rust usize
C bool/_Bool -> Rust bool
C char as byte -> Rust u8
C char as text -> Rust char / &str,看语义
C #define value -> Rust const
C const local object -> Rust let
C global object -> Rust static
C mutable local -> Rust let mut
C default local -> Rust let
这里最重要的不是把名字背下来,而是先区分三类东西:
- 外部格式字段:用明确宽度类型
- 内存长度和下标:用
usize - 文本、字节、字符:不要混成
char *
运算符和类型转换会继续沿着这条线处理 C 里的隐式转换、整数提升和 Rust 的显式 as。