C 里 typedef 很常见:
typedef uint32_t DeviceId;
typedef uint8_t Byte;
typedef int (*compare_fn)(const void *a, const void *b);
它能减少重复、隐藏复杂声明、给一个类型加上业务名字。但 typedef 不一定带来新的类型边界。很多时候它只是给原类型取了另一个名字。
Rust 里对应有两种常用写法:
type DeviceId = u32; // 类型别名,不是新类型
struct DeviceId(u32); // newtype,是新类型
这篇先把这条线分清楚:什么时候只是为了简化签名,什么时候是为了让编译器帮你挡住参数传错。
type 只是别名
C 里:
typedef uint32_t DeviceId;
void open_device(DeviceId id);
Rust 的第一层对应是 type:
type DeviceId = u32;
fn open_device(id: DeviceId) {
println!("open {id}");
}
调用时可以传 u32:
let raw: u32 = 7;
open_device(raw);
因为 DeviceId 只是 u32 的别名。它能让函数签名更可读,但不会让 DeviceId 和 u32 在类型检查上变成两种东西。
这一点和 C 的很多 typedef 用法接近:名字变了,底层类型还是原来的类型。
别名适合减少重复
type 很适合给复杂类型取短名字。
例如回调:
type Handler = fn(event: Event) -> Result<(), Error>;
或者一类常用结果:
type ParseResult<T> = Result<T, ParseError>;
fn parse_len(data: &[u8]) -> ParseResult<u16> {
if data.len() < 2 {
return Err(ParseError::TooShort);
}
Ok(u16::from_be_bytes([data[0], data[1]]))
}
这里 ParseResult<T> 没有创建新的返回类型。它只是把 Result<T, ParseError> 这个重复写法收起来。
这类别名的目的通常是:
- 缩短复杂类型
- 统一错误类型
- 让 API 签名更容易读
- 给 FFI 回调、函数指针或闭包类型取名字
别名挡不住参数传错
如果两个别名底层都是 u32,它们仍然能互相传:
type DeviceId = u32;
type UserId = u32;
fn read_device(id: DeviceId) {
println!("device {id}");
}
let user: UserId = 100;
read_device(user);
这段代码能通过,因为 DeviceId 和 UserId 都只是 u32。
C 里也常见这种问题:
typedef uint32_t DeviceId;
typedef uint32_t UserId;
void read_device(DeviceId id);
UserId user = 100;
read_device(user);
如果业务上 DeviceId 和 UserId 不能混用,别名不够。
newtype 创建真正的新类型
Rust 常用 tuple struct 表达 newtype:
struct DeviceId(u32);
struct UserId(u32);
现在它们是两种不同类型:
fn read_device(id: DeviceId) {
println!("device {}", id.0);
}
let user = UserId(100);
// read_device(user); // 类型不匹配
调用方必须传 DeviceId:
let device = DeviceId(7);
read_device(device);
这就是 newtype 的价值:运行时通常没有额外成本,但编译期多了一层业务边界。
给 newtype 加方法
裸 tuple struct 可以工作,但直接访问 .0 容易把封装又拆掉。更常见的是提供构造和访问方法:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DeviceId(u32);
impl DeviceId {
fn new(value: u32) -> Self {
Self(value)
}
fn value(self) -> u32 {
self.0
}
}
使用时:
let id = DeviceId::new(7);
println!("{}", id.value());
如果 ID 有取值约束,可以放进构造函数:
impl DeviceId {
fn new(value: u32) -> Option<Self> {
if value == 0 {
None
} else {
Some(Self(value))
}
}
}
这样“设备 ID 不能为 0”就不是注释,而是类型构造路径的一部分。
结构体 typedef 怎么对应
C 里常见:
typedef struct {
uint16_t port;
uint8_t retry;
} Config;
Rust 不需要用 type 给结构体取名,直接定义命名结构体:
struct Config {
port: u16,
retry: u8,
}
如果是 C 这种写法:
typedef struct Config Config;
它常用于隐藏结构体定义,只暴露不透明句柄。Rust 普通库里通常用模块可见性、私有字段和公开方法表达封装:
pub struct Config {
port: u16,
retry: u8,
}
impl Config {
pub fn new(port: u16, retry: u8) -> Self {
Self { port, retry }
}
pub fn port(&self) -> u16 {
self.port
}
}
字段不写 pub,外部就不能直接改字段,只能走你暴露的方法。
FFI 里别名和 newtype 要谨慎
C 头文件里可能有:
typedef uint32_t device_id_t;
如果 Rust 只是要和 C ABI 对齐,通常可以用别名:
type DeviceIdRaw = u32;
如果 Rust 内部 API 想防止混用,可以在边界处转换成 newtype:
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeviceId(u32);
#[repr(transparent)] 表示这个单字段包装类型在 ABI 表示上和内部字段兼容,常用于 FFI 边界附近的 newtype。普通业务 newtype 不一定需要它;只有当 ABI 表示也是契约时才写。
一条实用规则是:
靠近 C ABI:先保证布局和原始类型清楚
进入 Rust 内部:用 newtype 表达业务边界
什么时候用 type,什么时候用 newtype
用 type:
- 类型很长,只是想缩短签名
- 统一某类
Result<T, E> - 给函数指针、回调、闭包类型取名字
- 只是为了和 C 头文件里的名字对应
用 newtype:
- 两个底层类型相同的概念不能混用
- 需要给值加构造约束
- 需要给一个业务概念加方法
- 希望 API 签名表达更强的语义
- 想在 Rust 内部隔离原始整数、句柄、索引、ID
这一层先记住什么
C typedef 到 Rust 不只有一种翻译:
typedef uint32_t DeviceId -> type DeviceId = u32
需要真正区分 DeviceId/u32 -> struct DeviceId(u32)
复杂返回类型简写 -> type ParseResult<T> = Result<T, E>
结构体 typedef -> Rust 命名 struct
不透明结构体/封装 -> 私有字段 + pub 方法
FFI 单字段包装 -> #[repr(transparent)] newtype
type 解决的是名字和可读性;newtype 解决的是类型边界和业务约束。C 程序员迁移到 Rust 时,真正值得补上的能力通常不是“换个别名”,而是让编译器帮你区分那些本来只靠命名约定区分的值。