/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
介绍一种简单但常见的 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,这是我们唯一需要的信息,只在编译期有用和有效。
参考资料
- https://medium.com/@isuru.dex/brief-overview-of-rusts-newtype-design-pattern-8855a44d9e28





