跳转到主内容类型别名和 newtype | 物联网民工

类型别名和 newtype

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 时,真正值得补上的能力通常不是“换个别名”,而是让编译器帮你区分那些本来只靠命名约定区分的值。

继续阅读