Rust 设计模式:newtype


介绍一种简单但常见的 Rust 设计模式:newtype,这一模式在 Rust 中很常见,也算是一种常识性的术语了。newtype 是指将一个类型(比较典型的是基本类型,例如 i32, f64, String)包裹进一个元组结构体中,形成一个新的类型,通常会给这个新类型起一个更具有领域含义的名称。例如:

struct Email(String);

这种做法会有以下几点收益( 本文原文地址:https://laurence.blog.csdn.net/article/details/168145388 ):

1. 避免将参数搞混

在下面这个例子中你会注意到,amount 和 acc_no 都是整型,如果一不注意,就容易传错参数:

fn transfer(amount: i32, acc_no: i32) {
  // something...
} 

fn main() {
  let amount: i32 = 500;
  let acc_no: i32 = 6514;
  
  // swap args; works ok; incorrect result!
  transfer(acc_no, amount);    
}

使用 newtype 包裹之后,由于有类型检查,编译器可以帮助我们检查出这种错误:

struct Amount(i32);
struct AccNo(i32);

// pass structs as arguments
fn transfer(amount: Amount, acc_no: AccNo) {
  // do something
}

fn main() {
  transfer(Amount(500), AccNo(6514));
}

2. 增加易读性

这也是显而易见的,下面的示例都能印证这一点:

struct Meters(f64);
struct Centimeters(f64);
struct Inches(f64);
struct TotalLength(f64);

struct NodeIndex(usize);
struct EdgeIndex(usize);

3. 对包裹类型添加校验

这也很合理,作为特定值时,对基类型的格式、取值范围往往是有要求的,这些校验、格式化的方法最适合添加在 newtype 上,下面的示例是在初始化方法中添加了校验功能:

struct Email(String); 

impl Email {
    // validate when creating Email;
    pub fn new(value: String) -> Result<Self, String> {
        if value.contains('@') && value.contains('.') {
            Ok(Email(value))
        } else {
            Err("Missing the `@` and/or the `.`".into())
        }
    }
}

fn main() {
    let valid_email = Email::new("atlas@123.com".to_string());
    assert!(valid_email.is_ok());
}

4. 控制可见性 / 隐藏实现细节

newtype 的最后一种常见应用场景就是用来控制类型的可见范围。因为 newtype 是用一个新的类型去包裹一个已有的类型,利用定义类型时所处的位置(在哪个 crate 中)以及是否在前面添加 pub 关键字这些差别,就可以有效控制类型的可见性。例如下面这个例子:

pub struct UserName(pub String); // Username is public; String is public too;
pub struct Password(String); // Password is public; String remains hidden;
struct Email(String); // Email is only visible in crate.

实际上,通过控制原始类型和 newtype 类型的可见范围,可以创造出更多组合。在下一篇文章中,我们将会看到 Zed 项目中的一个真实而有力的 newtype 应用示例,在那个实例中就是利用了私有 newtype + 公开的 getter / setter 来控制某些全局状态的读写权限,请关注本公众号的下一篇文章。

最后,解释一下使用 newtype 是否会带来额外的开销,答案是:完全不会。我记得之前我们在《Rust “多态”的来龙去脉(下):dyn Trait ( Trait Object ) 到底是什么东西?》一文中介绍过结构体的内存布局:

根据《Programming Rust, 2nd Edition》第 9 章的介绍,Rust 要求结构体的各个字段统一存放在结构体的内存块中,每个字段保留其原始数据类型在内存中的布局,结构体并不会拆解字段的内存布局,不过 Rust 没有对结构体各个字段在内存中的顺序进行明确规定。

结构体占据的内存就是它所有字段占据的内存总和,结构体本身并不占据额外的内存,实际上,结构体“类型”在运行时基本不存在,字段名、字段类型这些定义只在编译期使用。编译完成后,访问字段被翻译成“起始地址 + 固定偏移量”的内存读写,运行时不需要查询任何结构体描述。所以,newtype 结构体是零成本的,其本身并不会额外占用内存,在编译时,编译器会生成它的 TypeId,这是我们唯一需要的信息,只在编译期有用和有效。


参考资料

posted @ 2026-10-17 14:08  十万公里通票  阅读(3)  评论(0)    收藏  举报