📚 Rust 课程系列
课程概览 基础语法(一):变量、数据类型与字符串 切片(Slice):序列的借用视图 基础语法(二):运算符、表达式与控制流 函数与输入输出 所有权、借用与生命周期 结构体 (本文)枚举 模式匹配 类型系统:泛型、trait 与多态 集合与容器 错误处理与 Panic 恢复 模块、属性与宏 智能指针、迭代器与闭包 并发与异步编程 Unsafe Rust 与常用 trait 详解 工具链、Cargo 与外部 crate 最佳实践、性能与调试 结构体是 Rust 组织数据的基本手段:把若干相关字段聚合成有名字、有类型的整体,让编译期就能检查「字段是否齐全、是否被错误复用」。相比 C 的结构体,Rust 结构体不仅承载数据,还通过 impl 块承载行为、通过 derive 宏批量获得常用能力、通过字段私有性天然支持封装——是一个「数据 + 行为 + 不变式」的完整单元。
本章与《枚举》 和《模式匹配》 同属「自定义类型」主题:结构体「并列地组合数据」,枚举「互斥地表达可能」,模式匹配则是处理枚举的惯用手段。
三种结构体形式 Rust 提供三种结构体形式,语法、字段访问方式和典型用途各异。选哪种,取决于「字段需不需要名字」「需不需要携带数据」「是否只是为了实现 trait」。
形式 语法 字段访问 典型用途 命名结构体 struct Name { field: T }x.field数据记录、业务实体、配置 元组结构体 struct Name(T1, T2)x.0 / x.1newtype 包装、坐标、颜色 单元结构体 struct Name;无字段 类型标记、trait 实现载体
命名结构体 命名结构体最常用:每个字段都有名字,构造与访问都通过字段名。字段名让代码自带文档–读 Rectangle { width, height } 立刻知道两个 u32 各代表什么,而 (u32, u32) 则要靠注释或上下文。
1 2 3 4 5 6 7 8 9 10 11 struct Rectangle { width: u32 , height: u32 , }let rect = Rectangle { width: 30 , height: 50 , };println! ("宽 = {}" , rect.width);
命名结构体适合字段较多、语义需名字承载的场景。字段超过 3-4 个,或类型相同容易混淆(多个 String、多个 u32)时,命名结构体几乎是唯一合理选择–元组结构体靠 .0、.1、.2 计数,可读性会迅速崩坏。
元组结构体 元组结构体有名字但字段无名,只能按位置 .0、.1 访问。核心价值是给已有类型套上「新名字」,让编译器区分「底层相同但语义不同」的值:
1 2 3 4 5 6 7 8 9 struct Color (u8 , u8 , u8 ); struct Point (f64 , f64 ); let orange = Color (255 , 165 , 0 );println! ("R = {}" , orange.0 ); let shanghai = Point (31.23 , 121.47 );println! ("lat = {}" , shanghai.0 );
🔄 对比 :元组结构体介于「带名结构体」和「裸元组」之间。裸元组 (u8, u8, u8) 无类型名,两个三元组可互相赋值,类型系统无法区分;元组结构体 Color(u8, u8, u8) 有独立类型名,Color 与 Point 即便底层都是三个 u8 也不能互相赋值。这是「用类型名换类型安全」的关键一步。
元组结构体最常见的用法是单字段 形式,即 newtype 模式(详见下节)。多字段形式适合「字段顺序天然有含义且数量很少」的场景,如坐标、颜色、复数–它们本就按分量顺序书写,用 .0/.1 访问反而更贴切。
单元结构体 单元结构体不携带数据,定义时连字段列表都没有,像一个「空的标记」:
1 2 3 4 5 struct AlwaysEqual ;struct RouterMarker ;let _unit = AlwaysEqual;
它看起来「什么都没有」,但在两个场景下不可或缺:
作为 trait 实现的载体 。有时想为某类型实现 trait 以获得行为,但该类型本身不需要状态。例如框架的路由标记、状态机的「空状态」、PhantomData<T> 的底层(它本身是单元结构体,在类型层面「假装」持有 T 而不占运行时空间)。作为类型级的「事件」或「信号」 。泛型代码里常作「类型参数的占位」,编译期参与类型计算,运行期零开销。1 2 3 4 5 6 7 8 9 10 use std::marker::PhantomData;struct HasLength ; struct NoLength ;struct MyContainer <T, Marker> { data: Vec <T>, _marker: PhantomData<Marker>, }
三种形式的取舍 三种形式并非语法糖等价。下面的决策流程可快速判断取舍:
flowchart LR
A["携带数据?"] -->|否| B["单元结构体"]
A -->|是| C["字段需名字?"]
C -->|是| D["命名结构体"]
C -->|否| E["元组结构体"]💡 提示 :常见误区是「元组结构体比命名结构体轻量」。实际上三者编译后的内存布局都由字段决定,元组结构体不省内存也不更快。选择形式的唯一依据是可读性与语义清晰度,非性能。
newtype 模式深入 元组结构体最重要的用途是 newtype 模式 :用新名字包装一个已有类型。它零开销(编译后就是被包装类型本身),却带来类型安全与封装两大收益。
零开销区分同底层不同语义 最直接的收益是让编译器区分「底层相同、语义不同」的值。两个函数都接收 f64,一个表示距离、一个表示时间,传反位置时编译器毫无办法:
1 2 3 4 5 6 7 fn speed (distance: f64 , time: f64 ) -> f64 { distance / time }
用 newtype 给每个语义一个独立类型,传反位置就会直接编译失败:
1 2 3 4 5 6 7 8 9 10 struct Meters (f64 );struct Seconds (f64 );fn speed (d: Meters, t: Seconds) -> f64 { d.0 / t.0 }let v = speed (Meters (100.0 ), Seconds (10.0 ));
Meters 与 Seconds 运行期就是两个 f64,无额外开销;编译期则是两个不同类型,编译器拒绝任何混淆。
NASA 火星气候轨道器案例 单位混淆不是学术演练,它炸过真实的航天器。1998 年发射的火星气候轨道器(Mars Climate Orbiter)1999 年 9 月抵达火星时因地面软件单位不匹配而解体:洛克希德·马丁用磅力秒(pound-force·seconds)计算推进器冲量,NASA 喷气推进实验室用牛顿秒(newton·seconds),1 磅力秒 ≈ 4.45 牛顿秒。两套数字在各自系统里都「正确」,却因底层都是浮点数、无类型区分被静默混用,导致轨道插入时探测器过低进入火星大气层烧毁。
如果当时有 newtype 模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct PoundForceSeconds (f64 );struct NewtonSeconds (f64 );impl PoundForceSeconds { fn to_newton (self ) -> NewtonSeconds { NewtonSeconds (self .0 * 4.44822162 ) } }fn apply_impulse (impulse: NewtonSeconds) { }let raw = PoundForceSeconds (100.0 );apply_impulse (raw.to_newton ());
混用直接编译失败,唯一能跨越的方式是显式调用 to_newton–每次转换都是一次「我确认要换单位」的人工确认。这正是 newtype 的精髓:把「容易出错的人肉记忆」变成「编译器强制的类型契约」 。
🔬 进阶 :newtype 的零开销有保证。Rust 承诺单字段元组结构体内存布局与被包装类型完全一致(repr(Rust) 下),即 size_of::<Meters>() == size_of::<f64>(),且无任何间接寻址。所以 newtype 既安全又免费。
封装不变式 newtype 的第二个收益是封装不变式:字段设私有,只暴露受控的构造器与方法,保证「该类型的每个实例都满足某条规则」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pub struct Port (u16 );impl Port { pub fn new (value: u16 ) -> Result <Self , String > { if value == 0 { Err ("端口不能为 0" .into ()) } else { Ok (Port (value)) } } pub fn value (&self ) -> u16 { self .0 } }
字段私有、只暴露 new/value,于是「端口永不为 0」这条不变式由类型系统强制保证–不存在「忘了校验」的实例。这种「构造即合法」的设计是 Rust 高可靠代码的基石。
与 Deref 的取舍 newtype 包装后,原类型方法不会自动可用:Meters(1.0) 不能直接调 f64 的 sqrt,得写 m.0.sqrt()。许多人图省事给 newtype 实现 Deref,让 m.sqrt() 自动解引用到 f64:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::ops::Deref;struct MyString (String );impl Deref for MyString { type Target = String ; fn deref (&self ) -> &String { &self .0 } }let s = MyString (String ::from ("hello" ));println! ("len = {}" , s.len ());println! ("upper = {}" , s.to_uppercase ());
这很方便,但 Deref 是为智能指针设计的,不是为 newtype 设计的 。无脑 impl Deref 带来两个问题:
破坏封装 。Deref 会把目标类型的所有 pub 方法暴露给你的 newtype。若封装 Port(u16) 是为限制接口,impl Deref for Port 会把 u16 的全部方法(wrapping_add、to_le_bytes、count_ones……)都泄露出去,封装形同虚设。语义混乱 。Deref 表达的是「智能指针指向目标,二者逻辑上是一回事」。MyString 与 String 也许算一回事,但 Meters 与 f64 显然不是–f64 能做 sin、log,Meters 取正弦毫无意义。Deref 让这些无意义方法也变得可调用,语义边界被打破。判断准则很简单:
当 newtype 与底层类型「语义上是同一个东西,只是换个名字或加层校验」,且你愿意暴露底层全部接口 时,impl Deref 合理(典型如 MyString(String))。 当 newtype 是「有自己语义边界的新类型」,底层只是恰好复用了表示时,不要 impl Deref,只显式实现真正需要的方法(典型如 Meters(f64)、Port(u16))。
flowchart LR
A["智能指针式容器?"] -->|是| B["impl Deref 合理"]
A -->|否| C["愿暴露全部方法?"]
C -->|是| D["impl Deref 可接受"]
C -->|否| E["不要 impl Deref"]⚠️ 注意 :Deref 还有「强制解引用」副作用–它会让 &MyString 自动转成 &String,在函数传参时静默发生。若 newtype 是为「绝不与底层类型互通」,这种静默转换正是要避免的。社区共识:Deref 留给智能指针(Box、Rc、Arc、String、Vec),newtype 优先显式方法。
构造与初始化 字段初始化简写 当局部变量与字段同名时,可省略重复的名字只写一次字段名。这是纯语法糖,编译后与 name: name 完全一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct User { username: String , email: String , active: bool , }fn build_user (username: String , email: String ) -> User { User { username, email, active: true , } }
简写在构造函数里尤为常见:参数名通常与字段名一致,简写能消除重复、减少打字错误。
结构体更新语法 基于已有实例创建新实例时,未显式指定的字段可用 ..old 从旧实例「搬」过来,避免重写所有字段:
1 2 3 4 5 6 7 8 9 10 let user1 = build_user ( String ::from ("alice" ), String ::from ("a@example.com" ), );let user2 = User { email: String ::from ("b@example.com" ), ..user1 };
..user1 表示「其余字段全部从 user1 取」,与字段简写搭配可简洁地「基于旧值派生新值」。
更新语法的移动语义细节 ..user1 不是「复制」,而是逐字段移动或复制 :对每个未显式列出的字段,Copy 类型按位复制,否则移动。这条规则带来一个常被忽视的后果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct User { username: String , email: String , active: bool , sign_in_count: u64 , }let user1 = User { username: String ::from ("alice" ), email: String ::from ("a@example.com" ), active: true , sign_in_count: 1 , };let user2 = User { email: String ::from ("b@example.com" ), ..user1 };
执行后,user1 处于部分移动 状态:username 被移走(不可再用),email 未被 ..user1 触及(显式指定,用新值)仍可访问,active 和 sign_in_count 是 Copy,复制后原值仍可用。但 user1 整体不能再作为完整值使用:
1 2 3 4 println! ("{}" , user1.email); println! ("{}" , user1.active);
⚠️ 注意 :常见误区是「只要用了 ..user1,user1 就一定废了」。是否废取决于有没有非 Copy 字段被搬走 。若所有未列出字段都是 Copy(如 bool、u32、f64),user1 仍完整可用;只要任一非 Copy 字段被搬走,user1 就进入部分移动状态。编译器会逐字段检查;理解这条规则能解释编译器报的某些「奇怪」错误。
一个全部 Copy 字段的例子:
1 2 3 4 5 6 struct Pixel { r: u8 , g: u8 , b: u8 } let p1 = Pixel { r: 10 , g: 20 , b: 30 };let p2 = Pixel { b: 99 , ..p1 }; println! ("p1 = {} {} {}" , p1.r, p1.g, p1.b);
结构体字面量作为表达式 结构体字面量是表达式,可出现在任何表达式位置–直接调用方法、作函数参数、嵌入更大的字面量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let area = Rectangle { width: 3 , height: 4 }.area ();fn is_square (r: Rectangle) -> bool { r.width == r.height }println! ("{}" , is_square (Rectangle { width: 5 , height: 5 }));struct Window { rect: Rectangle, title: String , }let win = Window { rect: Rectangle { width: 800 , height: 600 }, title: String ::from ("主窗口" ), };
💡 提示 :「字面量即表达式」让 Rust 结构体构造非常灵活,不像某些语言需要专门的 new 关键字或工厂方法。但含非 Copy 字段的字面量若只构造一次,构造完即移动,不会有多余拷贝。
字段访问、可变性与部分移动 整体可变,无字段级 mut Rust 的可变性是结构体级别 的:一个绑定要么 mut(所有字段都可改),要么不可变(所有字段都不可改)。不存在「只让某字段可变」的语法:
1 2 3 4 5 6 7 8 struct Point { x: i32 , y: i32 }let mut p = Point { x: 1 , y: 2 }; p.x = 10 ; p.y = 20 ; let q = Point { x: 1 , y: 2 };
🔄 对比 :C++ 允许 mutable 字段,C 允许 const 结构体里某字段单独非 const,Rust 不提供这种细粒度控制。这是有意的简化:字段级可变会让「这个值能不能改」依赖每个字段的声明,难以一眼看清。Rust 的哲学是「要么全可变,要么全不可变」,把可变性变成绑定级别的属性。
需要「部分可变」时,有两个惯用法:
拆分结构体 :把需独立可变的部分拆成独立结构体,分别持有 mut 绑定。内部可变性 :用 Cell/RefCell 让不可变结构体内部某些字段可变(详见《智能指针、迭代器与闭包》 )。1 2 3 4 5 6 7 use std::cell::Cell;struct Counter { count: Cell<u32 > }let c = Counter { count: Cell::new (0 ) }; c.count.set (c.count.get () + 1 );
部分移动 字段可单独 move 出来,这叫部分移动 。移动后被移出的字段不可再用,其他字段仍可用;结构体整体不能再作为完整值使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Person { name: String , age: u32 , }let p = Person { name: String ::from ("Alice" ), age: 30 , };let name = p.name; println! ("{}" , p.age);
部分移动适合「只想取出某字段、其余丢弃」,省去先 clone 再丢的浪费。Copy 字段不受影响(如 p.age 仍可读)。
💡 提示 :若整个结构体实现了 Copy(所有字段都是 Copy 时可 #[derive(Copy, Clone)]),赋值和传参都按位复制,不存在移动问题。但 String、Vec 等持有堆内存的类型无法 Copy,含这些字段的结构体也不能 derive Copy。能否 Copy 的判定见 derive 宏一节。
私有字段与封装 结构体字段默认对定义它的模块 可见,跨模块访问需显式 pub。这让结构体天然支持封装:内部表示可随意改,只暴露必要的访问器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 mod config { pub struct Config { pub port: u16 , secret_key: String , } impl Config { pub fn new (port: u16 , key: String ) -> Self { Self { port, secret_key: key } } pub fn has_key (&self ) -> bool { !self .secret_key.is_empty () } pub fn rotate_key (&mut self , new_key: String ) { self .secret_key = new_key; } } }use config::Config;let mut c = Config::new (8080 , String ::from ("s3cr3t" ));println! ("port = {}" , c.port); c.rotate_key (String ::from ("new" ));
🔬 进阶 :Rust 的可见性是「按模块」而非「按类」的。同一模块内所有代码都能访问彼此的私有字段,不论是否「同一个结构体的方法」。这与 Java/C++ 的 private(类内可见)不同,更接近 Python 的「约定式私有」(但 Rust 真的强制)。需更细粒度时,把类型放进子模块,用 pub(crate)、pub(super) 等限定可见范围。
方法与关联函数 impl 块与三种接收者 结构体的行为定义在 impl 块里。方法的第一个参数决定如何接收 self,共三种:
接收者 形式 语义 能否修改 不可变借用 &self只读访问,不获取所有权 否 可变借用 &mut self可读写,不获取所有权 是 获取所有权 self消耗实例,调用后不可再用 是(且销毁)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct Rectangle { width: u32 , height: u32 , }impl Rectangle { fn area (&self ) -> u32 { self .width * self .height } fn scale (&mut self , factor: u32 ) { self .width *= factor; self .height *= factor; } fn into_square (self ) -> Rectangle { let side = self .width.max (self .height); Rectangle { width: side, height: side } } }let mut r = Rectangle { width: 3 , height: 4 };println! ("area = {}" , r.area ()); r.scale (2 ); let s = r.into_square ();
选择接收者的原则:
默认 &self :只读操作最常见,也最不限制调用者。需改字段时 &mut self :调用者必须持有 mut 绑定。要把实例「转换并消耗」时 self :常见于 into_* 方法、链式终结方法。调用后实例不可再用,所有权转移到方法里。flowchart LR
A["需消耗实例转换?"] -->|是| B["self"]
A -->|否| C["需修改字段?"]
C -->|是| D["&mut self"]
C -->|否| E["&self"]多个 impl 块自动合并 一个类型可写多个 impl 块,Rust 会自动合并。这在按 trait 分组、按功能分文件时很有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 impl Rectangle { fn area (&self ) -> u32 { self .width * self .height } }impl Rectangle { fn perimeter (&self ) -> u32 { 2 * (self .width + self .height) } }let r = Rectangle { width: 3 , height: 4 };println! ("{}" , r.area ());println! ("{}" , r.perimeter ());
关联函数:构造器约定 不带 self 参数的函数叫关联函数 (associated function),相当于其他语言的「静态方法」。最常见的用途是构造器,约定俗成命名为 new:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 impl Rectangle { fn square (size: u32 ) -> Self { Self { width: size, height: size } } fn new (width: u32 , height: u32 ) -> Result <Self , String > { if width == 0 || height == 0 { Err ("宽高不能为 0" .into ()) } else { Ok (Self { width, height }) } } }let sq = Rectangle::square (10 );let r = Rectangle::new (3 , 4 ).unwrap ();
Self 在 impl 块内是「当前类型」的别名,等价于 Rectangle。关联函数通过 Type::name() 调用,如 String::from、Vec::new。
🔬 进阶 :new 只是约定,不是关键字,也不特殊。它不负责分配(Rust 没有专门的 new 运算符),就是一个普通关联函数。许多类型有多个构造器,按语义命名:Vec::with_capacity、String::from_utf8、OsString::from_string 等。new 通常留给「最常用、无失败」的构造路径。
方法调用链与自动引用 方法调用 r.area() 会根据接收者类型自动添加引用或解引用 ,无需手写 (&r).area() 或 (*r).area()。这套机制叫「自动引用/解引用」(auto-ref/deref):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 let r = Rectangle { width: 3 , height: 4 }; r.area ();let mut r2 = Rectangle { width: 3 , height: 4 }; r2.scale (2 );struct Builder { parts: Vec <String > }impl Builder { fn add (mut self , s: &str ) -> Self { self .parts.push (s.to_string ()); self } fn build (self ) -> String { self .parts.join ("," ) } }let s = Builder { parts: vec! [] } .add ("a" ) .add ("b" ) .add ("c" ) .build ();println! ("{}" , s);
自动引用的规则:编译器查找方法时,会依次尝试 r.method()、(&r).method()、(&mut r).method()、(*r).method() 等形式,找到匹配的就用。这就是为什么 String 能调用 str 的方法、Box<Rectangle> 能调用 Rectangle 的方法–Deref 配合自动引用让方法查找「穿透」多层包装。
derive 宏详解 Rust 的结构体默认几乎「什么都不会」:不能打印、不能比较、不能复制、不能哈希。这些能力都由对应 trait 提供,#[derive(...)] 宏能自动生成其中常用的几个实现。
逐个 trait 详解 derive 生成的 trait 作用 典型场景 DebugDebug{:?} / {:#?} 调试输出几乎总是需要 CloneClone显式深拷贝(.clone()) 需要复制含堆数据时 CopyCopy赋值/传参按位复制(隐式) 全字段 Copy 且无 Drop PartialEqPartialEq== / != 相等比较需要判等 EqEq自反相等(a == a) PartialEq 的强化HashHash可作为 HashMap/HashSet 的键 需要哈希 DefaultDefault提供默认值 部分更新、兜底
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] struct Point { x: i32 , y: i32 , }println! ("{:?}" , Point { x: 1 , y: 2 });let p1 = Point { x: 1 , y: 2 };let p2 = p1; println! ("{:?}" , p1); assert_eq! (p1, p2);use std::collections::HashSet;let mut set = HashSet::new (); set.insert (p1);let d = Point::default ();
Debug :生成 Debug trait 实现,让结构体可用 {:?}(单行)或 {:#?}(多行美化)打印。几乎所有结构体都该 derive Debug,调试和日志离不开它。derive 要求所有字段实现 Debug。
Clone :生成 Clone 实现,提供 .clone() 做显式深拷贝。含堆数据的字段(String、Vec),clone 会复制堆内容;Copy 字段则按位复制。derive 要求所有字段 Clone。
Copy :标记 trait,表示赋值和传参是「按位复制」而非「移动」。derive Copy 的前提是所有字段都是 Copy,且类型没有实现 Drop 。derive Copy 后,赋值 let p2 = p1 后 p1 仍可用–这正是「复制」与「移动」的区别。
PartialEq :生成 == / !=,逐字段比较。derive 要求所有字段 PartialEq。
Eq :PartialEq 的标记强化,承诺自反性(a == a 恒成立)。浮点数 f64 因 NaN != NaN 不满足自反性,只能 PartialEq 不能 Eq,所以含浮点字段的结构体也不能 derive Eq。
Hash :生成 Hash,让结构体能放进 HashMap/HashSet。derive 要求所有字段 Hash,且通常与 Eq 配套(HashMap 要求键 Eq + Hash)。
Default :生成 Default::default(),对每个字段调用其 Default。整型默认 0、bool 默认 false、String 默认空串、Option 默认 None。
Copy 的前提条件与代价 Copy 是最容易踩坑的 derive。它的前提是:
所有字段都是 Copy 。String、Vec、Box 等持有堆所有权的类型不是 Copy(它们实现了 Drop,复制会导致双重释放),所以含这些字段的结构体无法 derive Copy。类型没有手动实现 Drop 。Drop 表示「需自定义析构」,与「按位复制即完整」矛盾–复制后两个副本都析构,谁来释放堆?所以 Copy 与 Drop 互斥。1 2 3 4 5 6 7 8 9 10 #[derive(Copy, Clone)] struct Point { x: i32 , y: i32 }#[derive(Clone)] struct User { name: String , age: u32 , }
⚠️ 注意 :Copy 必须搭配 Clone(Copy: Clone 是 supertrait)。derive Copy 时通常写成 #[derive(Copy, Clone)]。单独 derive Copy 会报错。
Copy 的代价是「每次赋值/传参都复制」。对小类型(几十字节以内)几乎无感,但对大结构体(如含数 KB 数组)开销明显–每次传参都复制一份,且隐式难以察觉。所以 Copy 适合「小而值语义」的类型(坐标、颜色、标识符、小配置),不适合大结构体–后者用引用或 Clone 显式控制。
🔄 对比 :C++ 里拷贝构造函数是默认行为,且可隐式触发(传值、返回),大对象的隐式拷贝是经典性能陷阱。Rust 反过来:默认是移动(零成本),只有显式 derive Copy 的小类型才复制。这把「复制」从默认行为降级为需主动声明的特性,避免了 C++ 式的隐式大对象拷贝。
derive 的局限 derive 生成的实现是「机械」的:PartialEq 逐字段比较、Debug 逐字段打印、Hash 逐字段哈希。当类型语义不等于字段之和时,需手写实现。例如「两个 Point 即使字段不同也算相等」(极坐标与直角坐标),就得手写 PartialEq:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct PolarPoint { r: f64 , theta: f64 , }impl std ::cmp::PartialEq for PolarPoint { fn eq (&self , other: &Self ) -> bool { const TAU: f64 = std::f64 ::consts::TAU; (self .r - other.r).abs () < 1e-9 && (self .theta - other.theta).rem_euclid (TAU) < 1e-9 } }
Default trait 与部分更新惯用法 Default 提供类型的「自然默认值」。derive Default 会对每个字段调用其 Default,得到「全默认」实例。它的价值不在于「经常需要全默认值」,而在于配合结构体更新语法实现部分更新 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #[derive(Debug, Default)] struct ServerConfig { host: String , port: u16 , max_conn: usize , timeout_secs: u64 , debug: bool , }let cfg = ServerConfig { host: String ::from ("0.0.0.0" ), port: 8080 , ..Default ::default () };println! ("{:#?}" , cfg);
..Default::default() 是 Rust 里「部分初始化」的标准惯用法。它比「全字段写出默认值」简洁,又比「构造器一堆可选参数」类型安全–只写想改的字段,编译器保证其余字段有值。
手写 Default 时,应让默认值「安全且合理」:
1 2 3 4 5 6 7 8 9 10 11 12 impl Default for ServerConfig { fn default () -> Self { Self { host: String ::from ("127.0.0.1" ), port: 8080 , max_conn: 100 , timeout_secs: 30 , debug: false , } } }
💡 提示 :Default 还常用于测试夹具(生成「一个能用的实例」)、序列化兜底(缺字段时用默认)、以及 unwrap_or_default(Option 为 None 时取默认)。给业务结构体实现合理的 Default,是提升代码简洁度的低成本投入。
打印与格式化 Debug:{:?} 与 {:#?} Debug 是结构体最常 derive 的 trait,让结构体可用 {:?}(单行紧凑)或 {:#?}(多行美化)输出:
1 2 3 4 5 6 7 8 9 10 #[derive(Debug)] struct Point { x: i32 , y: i32 }let p = Point { x: 1 , y: 2 };println! ("{:?}" , p); println! ("{:#?}" , p);
{:?} 适合日志一行输出,{:#?} 适合人眼阅读复杂结构。derive Debug 要求所有字段 Debug,标准库类型基本都满足。
Display 的取舍 Rust 还有 Display trait,对应 {} 格式化,语义是「面向用户的友好输出」。但 Display 不能 derive ,必须手写,且通常只对「有自然文本表示」的类型有意义(如 String、IpAddr)。对结构体而言,derive Debug 用于调试,Display 用于对外展示,二者分工不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::fmt;struct Money { amount: i64 , currency: String , }impl fmt ::Display for Money { fn fmt (&self , f: &mut fmt::Formatter<'_ >) -> fmt::Result { write! (f, "{} {}" , self .amount, self .currency) } }let m = Money { amount: 9900 , currency: String ::from ("CNY" ) };println! ("{}" , m); println! ("{:?}" , m);
🔄 对比 :Python 的 __str__(用户友好)与 __repr__(调试)对应 Rust 的 Display 与 Debug。区别是 Python 都能轻松写,Rust 的 Display 必须手写且只对「有文本表示」的类型有意义–大多数结构体只 derive Debug 就够,Display 留给真正需要对外展示的类型。
手动实现 Debug 有时 derive 的 Debug 输出太冗长(如大数组字段),或想隐藏敏感字段(密码、密钥),就需手写 Debug:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use std::fmt;struct Credentials { username: String , password: String , }impl fmt ::Debug for Credentials { fn fmt (&self , f: &mut fmt::Formatter<'_ >) -> fmt::Result { f.debug_struct ("Credentials" ) .field ("username" , &self .username) .field ("password" , &"<redacted>" ) .finish () } }let c = Credentials { username: String ::from ("alice" ), password: String ::from ("s3cr3t" ), };println! ("{:?}" , c);
Formatter 提供 debug_struct、debug_tuple、debug_list 等辅助方法,让手写 Debug 不必逐字节拼字符串。隐藏敏感字段是安全代码的常见要求–日志里泄露密钥是真实事故来源,手写 Debug 是防御手段之一。
⚠️ 注意 :含引用字段的结构体若想 derive Debug,需给生命周期参数加约束 #[derive(Debug)] struct Ref<'a, T: Debug>(&'a T)。这是「trait 约束写在类型定义」的常见用法,详见《所有权、借用与生命周期》 与《类型系统》 。
内存布局 结构体在内存里如何摆放,直接关系性能与 ABI 兼容。Rust 给了从「让编译器自由优化」到「逐字节精确控制」的多个层次。
repr(Rust):字段重排与对齐填充 默认 repr(Rust) 下,编译器可自由重排字段 以最小化对齐填充。对齐(alignment)是「数据存放地址必须是某数倍数」的硬件要求:u32 对齐 4、u64 对齐 8、u8 对齐 1。字段顺序不佳会留下「填充」(padding)浪费空间:
1 2 3 4 5 6 7 8 9 10 use std::mem::size_of;struct Foo { a: u8 , b: u32 , c: u8 , }assert_eq! (size_of::<Foo>(), 8 );
重排前后的字节布局对比如下(假设起始地址 0,结构体对齐为 4):
1 2 3 4 5 6 7 8 9 10 11 12 13 源码顺序(若不重排): 偏移 0 1 2 3 4 5 6 7 8 9 10 11 +----+----+----+----+----+----+----+----+----+----+----+----+ | a |<-- pad 3B -->| b (u32) | c |<-- pad 3B -->| +----+----+----+----+----+----+----+----+----+----+----+----+ 总计 12 字节,5 字节填充浪费 repr(Rust) 重排后(b 提前,a/c 相邻): 偏移 0 1 2 3 4 5 6 7 +----+----+----+----+----+----+----+----+ | b (u32) | a | c |<- pad 2B->| +----+----+----+----+----+----+----+----+ 总计 8 字节,仅 2 字节填充
⚠️ 注意 :字段重排是合法优化,不要依赖字段的内存顺序或字段间地址差 。需固定布局时(FFI、网络协议、二进制格式解析)必须用 #[repr(C)]。在 safe Rust 里你也无法取字段地址做这种依赖(取引用得到的是字段地址,但偏移不保证稳定)。
#[repr©]:固定布局 #[repr(C)] 强制按声明顺序摆放字段,遵循 C 的对齐规则。用于需「跨语言 ABI 一致」或「二进制格式可预测」的场景:
1 2 3 4 5 6 7 8 9 10 11 use std::mem::{size_of, align_of};#[repr(C)] struct Header { magic: u32 , version: u16 , flags: u16 , }assert_eq! (size_of::<Header>(), 8 );assert_eq! (align_of::<Header>(), 4 );
#[repr(C)] 不改变字段对齐,只固定顺序。它最适合:
FFI :与 C 代码交互时,C 端按 struct { u32; u16; u16; } 摆放,Rust 端用 #[repr(C)] 保证一致。二进制解析 :解析文件头、网络包时,按字节布局精确匹配协议。#[repr(packed)]:慎用 #[repr(packed)] 完全去掉对齐填充,字段紧挨着摆放,最省空间,但代价是未对齐访问 :
1 2 3 4 5 6 #[repr(packed)] struct Packed { a: u8 , b: u32 , }assert_eq! (size_of::<Packed>(), 5 );
⚠️ 注意 :#[repr(packed)] 危险在于取字段引用。&packed.b 得到未对齐引用,而 &T 要求 T 对齐–safe Rust 里取 packed 字段引用会被编译器拒绝,必须 unsafe:
1 2 3 4 5 6 7 8 9 10 #[repr(packed)] struct Packed { a: u8 , b: u32 }let p = Packed { a: 1 , b: 0x04030201 };let val = unsafe { p.b.read_unaligned () };println! ("{}" , val);
未对齐访问在某些平台(ARM、MIPS)是 UB,在 x86 上虽能跑但慢一倍。所以 #[repr(packed)] 应仅用于必须匹配的硬件/协议布局 ,且读写都用 read_unaligned/write_unaligned,绝不在 packed 结构体上取普通引用。
零大小类型 ZST 零大小类型(Zero-Sized Type)是 size_of::<T>() == 0 的类型。单元结构体就是 ZST,此外 ()、[T; 0]、PhantomData<T> 都是:
1 2 3 4 5 6 7 use std::mem::size_of;struct Empty ; assert_eq! (size_of::<Empty>(), 0 );assert_eq! (size_of::<()>(), 0 );assert_eq! (size_of::<[i32 ; 0 ]>(), 0 );assert_eq! (size_of::<std::marker::PhantomData<u64 >>(), 0 );
ZST 的特殊之处:
不占内存 。一个 Vec<Empty> 装 100 万个单元结构体,实际只占几个字节元数据,元素本身零开销。复制和移动无开销 。零字节复制就是「什么都不做」。构造无开销 。创建 ZST 值不分配、不初始化。💡 提示 :ZST 在泛型代码里很有用。例如 HashMap<K, V> 当 V = () 时退化为 HashSet<K>,复用全部实现而零额外开销。PhantomData<T> 用 ZST 在类型层面「标记」一个类型参数,运行期不占空间。理解 ZST 能解释很多「为什么这看起来有开销却免费」的现象。
用 size_of / align_of 验证 标准库提供 size_of::<T>() 与 align_of::<T>() 验证布局,调试时还可用 cargo rustc -- -Zprint-type-sizes(nightly)打印每个类型的确切字节数与字段偏移。下面这段代码验证常见结构的布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use std::mem::{size_of, align_of};struct Mixed { a: u8 , b: u64 , c: u8 , }println! ("Mixed: size={}, align={}" , size_of::<Mixed>(), align_of::<Mixed>());#[repr(C)] struct MixedC { a: u8 , b: u64 , c: u8 , }println! ("MixedC: size={}, align={}" , size_of::<MixedC>(), align_of::<MixedC>());
🔬 进阶 :想亲眼看到字节的原始内容,可用 unsafe 把值转成字节切片。但这是诊断手段而非生产代码,且要注意 repr(Rust) 的布局不稳定,跨编译版本可能变化:
1 2 3 4 5 6 7 8 9 fn dump_bytes <T>(val: &T) { let bytes = unsafe { std::slice::from_raw_parts ( val as *const T as *const u8 , std::mem::size_of::<T>(), ) }; println! ("{:02x?}" , bytes); }
组合优于继承 Rust 没有继承(inheritance),既无数据继承也无实现继承。复用数据靠组合 (composition):把其他结构体作为字段嵌套进来;复用行为靠 trait (见《类型系统》 )。
has-a 嵌套 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Engine { horsepower: u32 , }struct Gps { lat: f64 , lng: f64 , }struct Car { engine: Engine, gps: Gps, model: String , }let car = Car { engine: Engine { horsepower: 300 }, gps: Gps { lat: 31.23 , lng: 121.47 }, model: String ::from ("Model S" ), };println! ("{} 匹马力" , car.engine.horsepower);
组合是显式的:Car 想用 Engine 的什么就写 car.engine.xxx(),不会出现「调一个方法不知道它来自哪层父类」的迷雾。
与 trait 配合复用行为 继承同时耦合了「数据复用、行为复用、类型层级」三件事,Rust 把它们拆开:数据用嵌套字段,行为用 trait,类型层级用 trait bound。需复用行为时,定义 trait 并为类型实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 trait Describable { fn describe (&self ) -> String ; }impl Describable for Engine { fn describe (&self ) -> String { format! ("{} 匹马力引擎" , self .horsepower) } }impl Describable for Car { fn describe (&self ) -> String { format! ("{}(搭载 {})" , self .model, self .engine.describe ()) } }println! ("{}" , car.describe ());
🔄 对比 :面对「Cat 是一种 Animal」的继承冲动,Rust 的答案是拆问题–共享数据用嵌套字段,共享行为用 trait。这规避了 C++ 菱形继承(多继承下基类重复)和 Java 深层继承链(Object -> Animal -> Mammal -> Cat -> …,方法来源难追踪)的问题。Go 的「嵌入字段」机制与 Rust 组合思路相近,也是把「is-a」改写成「has-a + 委托」。
DTO 与业务实体分离 组合哲学下,一个常见实践是「接口层 DTO 与内部业务实体分离」:DTO(Data Transfer Object)是轻量结构体,只含序列化所需字段;业务实体是富类型,含不变式与方法。两者通过显式转换沟通:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #[derive(Debug, serde::Serialize)] struct UserDto { id: u64 , name: String , email: String , }struct User { id: UserId, name: UserName, email: Email, }impl User { fn from_dto (dto: UserDto) -> Result <Self , String > { Ok (Self { id: UserId::new (dto.id)?, name: UserName::new (dto.name)?, email: Email::new (dto.email)?, }) } fn to_dto (&self ) -> UserDto { UserDto { id: self .id.value (), name: self .name.value (), email: self .email.value (), } } }
💡 提示 :DTO 与实体分离的好处是「内部表示可自由演进,外部接口稳定」。业务实体的字段可重命名、改类型、加不变式,只要 to_dto/from_dto 维持 DTO 形状,外部 API 就不受影响。反之,把内部实体直接序列化暴露,会让内部变更变成破坏性 API 变更。
Builder 模式 当结构体的可选字段很多、构造需校验、或想支持链式调用时,Builder 模式 是惯用法。它把「构造」从一次性字面量拆成「逐步设置 + 终结校验」两步,每步返回 builder 自身以支持链式调用。
完整的 Builder 实现(含 build 校验):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 #[derive(Debug, Clone)] pub struct ServerConfig { pub host: String , pub port: u16 , pub max_connections: usize , pub timeout_secs: u64 , pub debug: bool , }#[derive(Debug)] pub enum ConfigError { InvalidPort (String ), InvalidTimeout (String ), InvalidMaxConnections (String ), }impl std ::fmt::Display for ConfigError { fn fmt ( &self , f: &mut std::fmt::Formatter<'_ >, ) -> std::fmt::Result { match self { ConfigError::InvalidPort (msg) => { write! (f, "无效端口: {msg}" ) } ConfigError::InvalidTimeout (msg) => { write! (f, "无效超时: {msg}" ) } ConfigError::InvalidMaxConnections (msg) => { write! (f, "无效最大连接数: {msg}" ) } } } }impl std ::error::Error for ConfigError {}#[derive(Default)] pub struct ServerConfigBuilder { host: Option <String >, port: Option <u16 >, max_connections: Option <usize >, timeout_secs: Option <u64 >, debug: Option <bool >, }impl ServerConfigBuilder { pub fn new () -> Self { Self ::default () } pub fn host (mut self , host: impl Into <String >) -> Self { self .host = Some (host.into ()); self } pub fn port (mut self , port: u16 ) -> Self { self .port = Some (port); self } pub fn max_connections (mut self , n: usize ) -> Self { self .max_connections = Some (n); self } pub fn timeout_secs (mut self , secs: u64 ) -> Self { self .timeout_secs = Some (secs); self } pub fn debug (mut self , on: bool ) -> Self { self .debug = Some (on); self } pub fn build (self ) -> Result <ServerConfig, ConfigError> { let host = self .host .unwrap_or_else (|| "127.0.0.1" .to_string ()); let port = self .port.unwrap_or (8080 ); if port == 0 { return Err (ConfigError::InvalidPort ( "端口不能为 0" .into (), )); } let max_connections = self .max_connections.unwrap_or (100 ); if max_connections == 0 { return Err (ConfigError::InvalidMaxConnections ( "最大连接数不能为 0" .into (), )); } let timeout_secs = self .timeout_secs.unwrap_or (30 ); if timeout_secs == 0 { return Err (ConfigError::InvalidTimeout ( "超时不能为 0" .into (), )); } let debug = self .debug.unwrap_or (false ); Ok (ServerConfig { host, port, max_connections, timeout_secs, debug, }) } }fn main () { let cfg = ServerConfigBuilder::new () .host ("0.0.0.0" ) .port (3000 ) .max_connections (500 ) .timeout_secs (60 ) .debug (true ) .build (); println! ("{:#?}" , cfg); let bad = ServerConfigBuilder::new () .port (0 ) .build (); println! ("{:?}" , bad); }
Builder 的关键设计点:
Builder 字段全 Option ,初始为 None,表示「未设置」。设置器 mut self -> Self ,消耗并返回 builder,支持链式(b.host(..).port(..))。build 做校验 ,把 Option 解开为最终值,任一校验失败返回 Err,全部通过才构造 ServerConfig。目标结构体只在 build 里构造 ,外部无法绕过校验直接 ServerConfig { .. }(把字段设为私有即可,这里为演示用 pub)。💡 提示 :当字段很多、有校验、或构造逻辑复杂时用 Builder;字段少且无需校验时,直接字面量 + ..Default::default() 更简洁。Builder 不是银弹,是「构造复杂度足够高」时的工具。社区有 derive_builder、typed-builder 等 crate 自动生成 Builder,减少样板代码。
⚠️ 注意 :上面的 Builder 是「消耗式」的(每个设置器 mut self),调用一次后原 builder 不可再用。需「可复用 builder」时,设置器改为 &mut self -> &mut Self,build 改为 &self -> Result,但链式调用要写 &mut 借用,语法略繁。两种风格各有取舍,按需选择。
局部 struct 结构体可定义在函数体内,作用域仅限该函数。这适合「用完即弃」的临时分组–解析中间结果、测试夹具、临时聚合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fn parse_config (text: &str ) -> (bool , u32 ) { #[derive(Debug)] struct Entry { key: String , value: u32 , } impl Entry { fn is_enabled (&self ) -> bool { self .key == "enabled" } } let entry = Entry { key: String ::from ("enabled" ), value: 42 , }; (entry.is_enabled (), entry.value) }
局部 struct 的特点:
作用域限于函数 。Entry 在函数外无法命名,不能作返回类型或参数类型。临时类型需跨函数传递时,必须提升到模块级别。可 derive、可 impl、可实现 trait 。局部 struct 与模块级 struct 能力无差别,只是可见范围不同。不捕获环境变量 。与闭包不同,局部 struct 不会「捕获」外层变量,只能通过字段显式传入。不能捕获外层泛型参数 。fn f<T>() { struct S(T); } 非法–局部 struct 不能隐式使用外层泛型参数,需显式声明为泛型:struct S<T>(T)。1 2 3 4 5 6 7 8 9 10 11 fn f <T: Default >() { struct S <T>(T); let s = S (T::default ()); let _ = s; }
💡 提示 :局部 struct 在测试里特别有用–为单个测试构造「小型夹具类型」,用完即弃,不污染模块命名空间。也在解析器、状态机内部表达「这一步的中间结构」,让逻辑比裸用元组清晰。
常见模式速查表与陷阱小结 常见模式速查 模式 做法 收益 newtype struct UserId(u64)零开销区分同底层不同语义 封装不变式 字段私有 + pub 构造器 非法状态无法从外部构造 构造器约定 impl T { fn new(..) -> Self }集中校验,保证实例合法 Builder 链式 Config::new().x(..).build() 可选参数多时的可读构造 部分更新 { x, ..Default::default() }只写关心的字段 #[derive]Debug, Clone, PartialEq 等一行获得常用行为 DTO 分离 接口层 DTO 与内部实体分开 避免暴露内部可变状态 类型标记 单元结构体 + PhantomData 编译期类型计算,运行期零开销 委托方法 car.engine.xxx() 显式调用组合代替继承,行为来源清晰
常见陷阱 更新语法搬空实例 。..user1 会移动非 Copy 字段,导致 user1 部分移动后整体不可再用。误以为「只是复制」会在后续使用 user1 时遇编译错误。记住:只有全 Copy 字段时 user1 才完整存活。
无脑 impl Deref 。给 newtype impl Deref 会暴露底层全部方法,破坏封装、混淆语义。Deref 留给智能指针,newtype 优先显式方法。
packed 取引用 。#[repr(packed)] 结构体取字段引用是未对齐访问,safe Rust 会拒绝,unsafe 下可能 UB。packed 仅用于匹配硬件/协议布局,读写用 read_unaligned。
依赖字段内存顺序 。repr(Rust) 下编译器会重排字段,不要假设字段顺序或偏移。需固定布局用 #[repr(C)]。
大类型 derive Copy 。Copy 让赋值隐式复制,大结构体每次传参都拷贝。Copy 只适合小而值语义的类型,大对象用引用或显式 Clone。
含 String 却想 derive Copy 。String 持有堆内存,非 Copy,含它的结构体无法 derive Copy。需「复制」就 derive Clone 显式 .clone()。
字段级 mut 期待 。Rust 不支持「只让某字段可变」,要么整个 mut,要么用 Cell/RefCell 内部可变性。
局部 struct 捕获泛型 。fn f<T>() { struct S(T); } 非法,局部 struct 不能隐式用外层泛型参数,需显式 struct S<T>(T)。
Display 当 Debug 用 。Display({})不能 derive,且只对「有文本表示」的类型有意义。调试输出用 Debug({:?}),不要为每个结构体硬写 Display。
unwrap 当默认 。Builder 的 build 应返回 Result,而非 unwrap。把校验失败变成 panic 会把「可恢复错误」退化成「运行时崩溃」。
实战示例:HTTP 服务器配置 把本章的 newtype、封装不变式、Builder、组合、derive 串起来,写一个完整的 HTTP 服务器配置模块。这个例子综合体现「构造即合法」「部分更新」「链式构造」「DTO 分离」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 use std::fmt;pub struct Port (u16 );impl Port { pub fn new (value: u16 ) -> Result <Self , String > { if value < 80 { Err ("端口必须 >= 80" .into ()) } else { Ok (Port (value)) } } pub fn value (&self ) -> u16 { self .0 } }pub struct Host (String );impl Host { pub fn new (s: impl Into <String >) -> Result <Self , String > { let s = s.into (); if s.is_empty () { Err ("host 不能为空" .into ()) } else { Ok (Host (s)) } } pub fn as_str (&self ) -> &str { &self .0 } }pub struct ThreadPool { workers: usize , queue_size: usize , }impl ThreadPool { fn new (workers: usize , queue_size: usize ) -> Self { Self { workers, queue_size } } fn describe (&self ) -> String { format! ("{} workers / queue {}" , self .workers, self .queue_size) } }pub struct HttpServerConfig { host: Host, port: Port, pool: ThreadPool, debug: bool , max_body_bytes: u64 , }impl HttpServerConfig { pub fn listen_addr (&self ) -> String { format! ("{}:{}" , self .host.as_str (), self .port.value ()) } pub fn pool_info (&self ) -> String { self .pool.describe () } pub fn is_debug (&self ) -> bool { self .debug } pub fn max_body (&self ) -> u64 { self .max_body_bytes } }#[derive(Default)] pub struct HttpServerConfigBuilder { host: Option <String >, port: Option <u16 >, workers: Option <usize >, queue_size: Option <usize >, debug: Option <bool >, max_body_bytes: Option <u64 >, }impl HttpServerConfigBuilder { pub fn new () -> Self { Self ::default () } pub fn host (mut self , h: impl Into <String >) -> Self { self .host = Some (h.into ()); self } pub fn port (mut self , p: u16 ) -> Self { self .port = Some (p); self } pub fn workers (mut self , n: usize ) -> Self { self .workers = Some (n); self } pub fn queue (mut self , n: usize ) -> Self { self .queue_size = Some (n); self } pub fn debug (mut self , on: bool ) -> Self { self .debug = Some (on); self } pub fn max_body (mut self , bytes: u64 ) -> Self { self .max_body_bytes = Some (bytes); self } pub fn build (self ) -> Result <HttpServerConfig, String > { let host = match self .host { Some (h) => Host::new (h)?, None => Host::new ("0.0.0.0" )?, }; let port = match self .port { Some (p) => Port::new (p)?, None => Port::new (8080 )?, }; let workers = self .workers.unwrap_or (4 ); if workers == 0 { return Err ("workers 不能为 0" .into ()); } let queue_size = self .queue_size.unwrap_or (100 ); let pool = ThreadPool::new (workers, queue_size); let debug = self .debug.unwrap_or (false ); let max_body = self .max_body_bytes.unwrap_or (1024 * 1024 ); Ok (HttpServerConfig { host, port, pool, debug, max_body_bytes: max_body, }) } }#[derive(Debug, serde::Serialize)] pub struct HttpServerConfigDto { host: String , port: u16 , workers: usize , queue_size: usize , debug: bool , max_body_bytes: u64 , }impl HttpServerConfig { pub fn to_dto (&self ) -> HttpServerConfigDto { HttpServerConfigDto { host: self .host.as_str ().to_string (), port: self .port.value (), workers: self .pool.workers, queue_size: self .pool.queue_size, debug: self .debug, max_body_bytes: self .max_body_bytes, } } }impl fmt ::Debug for HttpServerConfig { fn fmt (&self , f: &mut fmt::Formatter<'_ >) -> fmt::Result { f.debug_struct ("HttpServerConfig" ) .field ("listen" , &self .listen_addr ()) .field ("pool" , &self .pool_info ()) .field ("debug" , &self .debug) .finish () } }fn main () { let cfg = HttpServerConfigBuilder::new () .host ("127.0.0.1" ) .port (3000 ) .workers (8 ) .queue (200 ) .max_body (2 * 1024 * 1024 ) .debug (true ) .build (); match cfg { Ok (c) => { println! ("{:?}" , c); println! ("DTO: {:?}" , c.to_dto ()); } Err (e) => println! ("构造失败: {e}" ), } let bad = HttpServerConfigBuilder::new () .port (22 ) .build (); println! ("{:?}" , bad); }
这个例子串起了本章的核心:
newtype (Port、Host)封装不变式,构造即合法,外部无法绕过校验。组合 (HttpServerConfig 含 Host/Port/ThreadPool)复用数据,行为来源清晰。Builder 把复杂构造拆成链式设置 + 终结校验,可选字段用默认值兜底。DTO 分离 让对外接口稳定,内部实体可自由演进。手写 Debug 控制输出,隐藏内部细节。结构体是 Rust 表达「带不变式的数据 + 行为」的核心载体。下一章《枚举》 讨论「互斥地表达可能」,与本章的「并列地组合数据」共同构成 Rust 自定义类型的两大支柱。