结构体定义与实例化
我们从基本类型和所有权系统一路走来,已经掌握了数字、字符、布尔值、数组和切片这些内建的数据容器。但在真实程序中,数据往往以更复杂的形态出现:一个用户有名字、年龄和邮箱,一本书有标题、作者和页数。把这些彼此关联的字段捆绑成一个语义完整的整体,是组织代码的第一步。结构体(struct)正是 Rust 提供的第一个自定义命名数据类型。
定义结构体
结构体通过struct关键字引入,后跟结构体名称和一组具名字段。字段的语法是字段名: 类型,多个字段之间用逗号分隔:
structUser{username:String,email:String,sign_in_count:u64,active:bool,}这条定义声明了一个名为User的类型,它包含四个字段,每个字段都有明确的类型。这里的String保存在堆上,其所有权归属于结构体实例;u64和bool直接内联存储。结构体没有在堆上额外分配空间——它的内存布局就是所有字段的叠加(不考虑对齐填充时)。
结构体名称遵循大驼峰命名法(UpperCamelCase),字段名遵循蛇形命名法(snake_case)。这不是编译器强制要求,而是 Rust 社区的通用约定,遵循它能显著提升代码的可读性。
定义结构体只是声明了类型的存在,它是模板而非数据。要真正使用它,需要创建实例。
实例化与字段初始化简写
创建结构体实例的语法是:写出结构体名称,后跟一个花括号块,块内以字段名: 值的形式为每个字段提供初值:
letuser1=User{email:String::from("alice@example.com"),username:String::from("alice"),active:true,sign_in_count:1,};字段的赋值顺序没有要求,这一点与部分语言不同。编译器会在编译期检查每个字段是否都已被赋值——少写一个字段、拼错字段名,都会产生编译错误。这种严格的静态检查正是 Rust 追求安全性的体现:类型系统在数据进入运行期之前就完成了完整性验证。
一个常见的模式是:局部变量恰好与字段同名。在从函数参数构造结构体时,这种重复尤为刺眼:
fncreate_user(username:String,email:String)->User{User{username:username,email:email,active:true,sign_in_count:1,}}username: username这样的写法虽然语义清晰,却在视觉上制造了噪音——字段名和变量名各写一遍,纯粹是文字的重复。Rust 为此提供了字段初始化简写(field init shorthand):当字段名与变量名完全一致时,可以省略冒号和变量名,只写一次字段名:
fncreate_user(username:String,email:String)->User{User{username,// 等价于 username: usernameemail,// 等价于 email: emailactive:true,sign_in_count:1,}}这条语法规则没有引入任何新的语义——它只是让你少敲几次键盘。但它的价值在于消除了表达上的冗余:当你看到username时,你看到的既是一个字段,也是一个同名的局部变量,编译器帮你完成了这层对应关系的接线。在构造包含五六个字段的结构体时,这种简写能显著减少代码的视觉密度。
简写规则只适用于具名字段结构体。元组结构体本身没有字段名,构造时只能通过位置传值,不存在简写的可能性。
结构体更新语法
另一种常见的构造场景是:基于一个已有的实例,修改其中部分字段,其余字段保持不变。假设我们有了一个已注册的用户user1,现在要创建一个同用户名、但邮箱不同且登录状态重置的新用户:
letuser2=User{email:String::from("bob@example.com"),active:true,..user1};这里的..user1就是结构体更新语法(struct update syntax)。它类似于其他语言中的"展开"或"拷贝剩余字段",但有一个关键区别需要留意:..执行的是移动或拷贝语义,具体取决于字段类型——active(bool)和sign_in_count(u64)实现了Copy,会被按位复制;而username(String)是移动而非复制,其所有权从user1转移给了user2,user1随后不能再访问username字段。
这意味着一个微妙的后果:如果user1的所有字段都实现了Copy(比如一个全部由整数构成的坐标结构体),那么更新后user1依然完全可用。但只要有一个字段是移动语义(如String),user1就会部分失效。
更新语法让"基于已有值派生新值"的表达变得极其紧凑:在..之前列出的字段是差异,..之后的值是基线。这种"差异 + 基线"的构造模式和 Git 的分支思想有异曲同工之妙——差异显式写出来,其余从基线上自动继承。
字段访问与所有权
访问结构体实例的字段使用点号(.)语法:
println!("{} has signed in {} times",user1.username,user1.sign_in_count);user1.username直接取出该字段的值。如果字段类型实现了Display,则可以像这里的String和u64一样直接参与格式化输出。
当实例被声明为可变时,可以通过赋值表达式修改字段:
letmutuser3=User{email:String::from("carol@example.com"),username:String::from("carol"),active:false,sign_in_count:0,};user3.active=true;// 合法:user3 是可变的user3.sign_in_count+=1;// 使用复合赋值运算符注意,字段级别的可变性不独立存在——let user声明的是绑定不可变还是一般可变,这个属性决定了其所有字段的读写权限。想要修改某个字段,必须将整个实例声明为mut。这个设计保持了 Rust 在可变性上的简单一致:可变性是属于绑定的属性,而非数据结构的属性。
字段访问还遵循所有权规则:访问一个拥有所有权的字段(如String)时会移动该字段,此后原实例不能再通过点号访问这个字段。访问实现了Copy的字段(如u64、bool)则是复制,原实例继续可用。这是所有权系统在结构体内部的第一次实战应用——结构体作为一个整体拥有其所有字段的所有权,而字段间的获取规则与独立变量完全一致。
至此,我们已经能用struct定义类型、创建实例、读写字段。一种复合数据的基本单元已经成形。但结构体的真正威力在于它不仅承载数据,还能承载行为——这正是下一节impl块中方法(method)与关联函数(associated function)将要解决的事。
元组结构体与单元结构体
结构体并不是只有具名字段这一种形态。前一小节中User的每个字段都有明确的名称——username、email在访问时可以自解释其含义。但有些场景下,字段的名称并无必要:一个二维坐标(x, y),一个 RGB 颜色(r, g, b),字段的语义已经被位置和类型充分表达。Rust 为此提供了结构体的第二种形态:元组结构体(tuple struct)。
元组结构体
元组结构体在名字上就揭示了它的本质——它像一个元组,但披着结构体的外衣。定义时只需要声明字段类型,不需要字段名:
// 二维坐标:两个 i32 字段,语义由位置决定structPoint(i32,i32);// RGB 颜色:三个 u8 字段structColor(u8,u8,u8);fnmain(){letorigin=Point(0,0);letred=Color(255,0,0);}实例化元组结构体时,传入的实参按位置依次对应字段类型。访问其中的字段,使用的是点号加索引。这里与具名字段结构体的关键区别在于:索引从0开始,不是从1开始,这是元组访问语法(tuple.0)的自然延续——元组结构体本质上就是一个有名字的元组:
fnmain(){letmutpoint=Point(3,5);println!("x = {}, y = {}",point.0,point.1);// 输出: x = 3, y = 5// 字段可变性规则与具名字段结构体一致:实例可变,字段即可变point.0=10;println!("新的 x = {}",point.0);// 输出: 新的 x = 10}这里有一个容易混淆的点值得明确指出:Point和Color虽然内部字段类型完全相同,但它们是不兼容的两种类型。你不能把一个Point赋值给一个声明为Color的变量,即使两者底层都是(i32, i32)或(u8, u8, u8)。这是元组结构体区别于裸元组的核心价值——它为你提供了一层类型层面的防护。考虑下面的对比:
// 裸元组:类型上没有约束,编译器不区分含义letred:(u8,u8,u8)=(255,0,0);letsize:(u8,u8,u8)=(800,600,0);// 编译器完全不介意// size 和 red 可以互相赋值,语义完全混淆// 元组结构体:类型系统替你区分语义letred2:Color=Color(255,0,0);letsize2:Size=Size(800,600);// 如果还有 Size 类型// red2 和 size2 之间直接赋值会编译报错——这正是我们想要的在第一小节中我们提到,具名字段结构体的最大优势是字段名自带文档。而元组结构体的取舍在于:用位置的简洁换取字段名的自解释性。因此,它最适合那些字段数量少(通常 2~3 个)、语义高度依赖位置、且你需要和裸元组区分开的场景。
元组结构体的另一个特性是它可以被解构(destructure),这一点你在第 9 节学习模式匹配时会深入体验。这里先看一眼解构的基本形态,为你留一个印象:
fnmain(){letPoint(x,y)=Point(3,5);println!("({}, {})",x,y);// 输出: (3, 5)}单元结构体
单元结构体是结构体中最轻量的一种形态,定义时甚至没有任何字段:
structEmpty;注意这里的写法——struct Empty;以分号结尾,没有花括号。这就是单元结构体(unit struct)。它之所以叫「单元」,是因为它和 Rust 中的单元类型()有相似之处:都是不携带任何数据、仅表达「一个类型存在」这一事实。
单元结构体的价值不在于存储数据,而在于定义类型本身。在所有权和借用那一节你已经体会到,Rust 的类型系统是编译期行为的核心依据。单元结构体最常见的用途,就是创建不需要数据、但需要一个独立类型来参与的上下文。比如一个后端的回调接口,某些状态下你只需要「这个类型存在」这一事实,而不需要它存储任何数据:
// 定义一个任务处理器,目前不需要任何状态structTaskHandler;implTaskHandler{fnrun(&self){println!("执行任务处理逻辑");}}fnmain(){lethandler=TaskHandler;// 实例化:不需要任何参数handler.run();// 调用方法}实例化单元结构体不需要使用()元组语法,直接写类型名就是一个值。它是零大小的类型,Rust 会保证每个单元结构体实例占用的内存为 0 字节。
此外,单元结构体常被用作「类型标记」(type marker)嵌入泛型设计中,这一点虽然超出本小节的范畴(泛型 impl 将在后续展开),但你可以记住这个方向:当需要在类型层面区分两个运行时行为完全相同的逻辑时,单元结构体是零成本的标记手段。
三种结构体形态的适用场景
至此,三种结构体形态已经齐备。它们之间的选择标准并不复杂,用一张对比表可以归纳清楚:
| 形态 | 定义语法 | 字段访问 | 适用场景 |
|---|---|---|---|
| 具名字段结构体 | struct S { name: T } | s.name | 字段多、字段名有自解释价值 |
| 元组结构体 | struct S(T1, T2) | s.0,s.1 | 字段少、语义靠位置承载 |
| 单元结构体 | struct S; | 无字段 | 只需要类型本身,不携带数据 |
一个值得记住的实践准则是:当你犹豫要不要用元组结构体时,先问自己「这里的字段名真的没有价值吗?」。大多数时候,具名字段结构体的可读性优势是值得多敲几个字符去换取的;元组结构体应该用在那些结构足够固定、语义足够清晰的少量场景中。而单元结构体则只在你需要类型标记或零状态上下文时才登场。
从具名字段结构体到元组结构体,再到单元结构体,我们已经完整掌握了如何用struct定义组织数据的骨架。但到目前为止,这些结构体都只是一个被动的数据容器——我们访问其字段、读取其值,却还没有让它们「行动起来」。下一节,我们将学习如何通过impl块为结构体赋予方法,让数据不仅被存储,更被行为所驱动。
结构体方法与关联函数
前两节构建了结构体的三种形态——具名字段、元组字段和空字段——但我们还只停留在"数据容器"的层面。一个User结构体虽然能装下用户名和邮箱,却无法回答"这个用户能否登录"或"邮箱是否合法"这类行为问题。Rust 通过impl块将行为与类型绑定:它允许我们把与类型紧密相关的函数组织在一起,让类型不只是被动的数据,而是能够响应操作的行为主体。
impl块:将函数绑定到类型
impl(implementation)块的语法非常直白:在impl关键字后跟上类型名,然后用一对花括号包住若干函数。先前提到的User结构体,我们可以立即为它添加第一个方法:
structUser{username:String,email:String,sign_in_count:u64,active:bool,}implUser{fnis_active(&self)->bool{self.active}}这里is_active接收一个&self参数——它会在运行时返回结构体实例的active字段值。关键在于impl块内的函数天然与类型绑定,不需要额外修饰;一旦离开impl块,is_active就不存在于普通作用域中,必须通过具体实例调用。
impl块允许定义多个函数,但它们共享同一个语义上下文。你可以将方法按逻辑分组——一个impl块放认证相关的方法,另一个放资料展示相关的方法——这是 Rust 允许的行为,它给予了组织代码的自由。
方法:self的三种形态
方法的第一参数是self,它决定了调用时所有权如何转移。Rust 提供三种形态,每种都有明确的语义含义:
| 形态 | 签名 | 语义 | 调用后实例状态 |
|---|---|---|---|
| 所有权方法 | fn into_name(self) | 消费实例,夺取其字段 | 实例被移动,不可再用 |
| 可变借用 | fn update(&mut self) | 修改字段,但不释放 | 实例仍然存在且可变 |
| 不可变借用 | fn is_active(&self) | 只读访问 | 实例不受影响 |
实际使用中,&self最为常见——实现只读查询。&mut self用于修改字段。self则用于需要从实例中提取数据的场景,例如把User转换为仅含用户名的轻量结构:
implUser{// 获取用户名所有权,消费掉 User 实例fninto_username(self)->String{self.username}}调用user.into_username()后,原user不能再被使用——所有权已转移。这与第 5 节所有权转移的语义完全一致:方法同样遵循移动规则。选择哪种self形态实际上是所有权设计的一部分,需要谨慎权衡。
调用语法十分统一:实例.方法名(参数)。Rust 自动引用或解引用以匹配签名——你不需要手动写(&user).is_active()这样的代码,直接user.is_active()即可,编译器自动完成借用。这也是 Rust 中"方法调用语法糖"的核心机制,让代码保持简洁的同时不牺牲安全。
关联函数:不依赖实例的构造器
impl块中的函数并非都接收self。如果函数签名中没有self,它就变成了关联函数(associated function)——通过类型::函数名调用。最常见的用途是构造函数:
implUser{fnnew(username:String,email:String)->Self{User{username,email,sign_in_count:1,active:true,}}}此处的Self是impl块中类型的别名——写Self与写User完全等价,但Self更简洁。User::new(...)返回一个全新的实例。关联函数不依赖具体实例存在,因此无法通过.调用,只能用::路径语法。
new只是一个约定俗成的名字,Rust 语言本身不强制要求任何构造函数名。你可以定义User::from_email(email)或User::default(),只要它是无self的函数即可。不过,社区习惯将最常用的构造入口命名为new,目的是降低阅读者的认知负担。
关联函数与方法的边界清晰:有关self的称为方法,无self的称为关联函数。不过,Rust 中的关联函数可以返回Self并通常用于构造,但也可以返回其他类型(如验证布尔值)。它的本质只是将逻辑挂到类型上,是否构造取决于你的设计。
组合与应用
有了方法和关联函数,一个结构体就能完整地表达"数据 + 行为"的封装。再回到Point元组结构体的例子,看看如何为它补上计算距离的方法:
structPoint(f64,f64);implPoint{// 关联函数:构造新点fnnew(x:f64,y:f64)->Self{Point(x,y)}// 方法:计算到原点的距离(只读)fndistance_from_origin(&self)->f64{(self.0*self.0+self.1*self.1).sqrt()}// 方法:平移点(可变借用)fntranslate(&mutself,dx:f64,dy:f64){self.0+=dx;self.1+=dy;}}// 调用实例letmutp=Point::new(3.0,4.0);println!("距离: {}",p.distance_from_origin());// 5.0p.translate(1.0,-1.0);println!("平移后: ({}, {})",p.0,p.1);// (4.0, 3.0)注意println!中我们通过p.0直接访问元组字段,这符合第 2 节中学到的点号加索引的语法。而方法内部通过self.0访问同一字段。这清晰地展示了方法体与调用方使用相同字段访问规则,一致性让代码更易推断。
设计建议:优先为结构体提供关联函数new作为唯一的构造入口,这能统一实例化的方式,避免散落的字段赋值。同时,只读查询一律用&self,只有需要修改内部状态才用&mut self,而self则留给"实例生命周期终结"的场景。
现在我们已经掌握了结构体定义、实例化和方法绑定,下一步的核心问题是:当我们拿到一个结构体实例,如何根据其不同形态或状态执行不同逻辑?这正是下一节——模式匹配与解构——要解决的问题。
枚举定义与变体
前三节我们构建了结构体的完整图景:具名字段让数据有了可读性,元组字段让紧凑的坐标和颜色有了简洁表达,impl块则赋予了类型行为。但结构体回答的始终是"一个值同时拥有哪些属性"这个问题,而真实世界还有另一类常见需求——一个值在多种形态之间选择一种。一个网络消息可能是请求也可能是响应;一个形状可能是圆形也可能是矩形;一张牌的花色只有四种可能。Rust 用枚举(enum)来建模这一场景,它允许我们定义一个类型,并明确列出这个类型所有可能的值。
枚举变体:类型的有限集合
枚举通过enum关键字声明,花括号内列出所有可能的变体(variant)。变体之间用逗号分隔:
enumIpAddrKind{V4,V6,}IpAddrKind类型的值只能是V4或V6二者之一。这就把"可能性的集合"显式地写进了类型系统——编译器会替我们保证,任何IpAddrKind类型的变量,都不可能落到这两个变体之外。这种穷尽性(exhaustiveness)是枚举最核心的承诺,我们会在模式匹配一节看到它带来的全部力量。
变体的命名遵循与结构体相同的大驼峰命名法,这与结构体的风格保持一致,让代码在视觉上就能快速区分"类型"与"值"。定义枚举时,每个变体之间用逗号分隔,最后一个变体后的逗号可以省略,但保留它是一种常见风格,便于后续追加新变体时减少 diff 噪声。
变体携带数据:从"标签"到"容器"
上面IpAddrKind的两个变体是不带数据的——它们只是纯粹的标签。但枚举的真正威力在于:每个变体可以携带不同类型和数量的数据。这使得枚举从"有限集合"升级为"能区分形态、且每种形态自带载荷"的复合类型。
enumIpAddr{V4(String),// V4 变体携带一个 StringV6(String),// V6 变体携带一个 String}这里V4不再是一个孤立的标签,而是携带了一个String类型的 IP 地址。当我们创建一个V4变体的值时,这个值同时包含了"它是 V4"这个身份信息,以及具体的地址数据:
lethome=IpAddr::V4(String::from("127.0.0.1"));letloopback=IpAddr::V6(String::from("::1"));更进一步的,不同变体可以携带不同种类的数据。一个变体可以携带元组,另一个可以携带结构体,甚至可以携带另一个枚举:
enumMessage{Quit,// 不带数据Move{x:i32,y:i32},// 携带匿名结构体Write(String),// 携带一个 StringChangeColor(u8,u8,u8),// 携带三个 u8}Move变体携带了一个匿名的结构体——它有字段名和类型,但不需要单独定义一个结构体类型。这种"变体内嵌数据形态"的能力,让枚举可以描述层次复杂的数据结构。仔细想想,Message枚举实际上把四种不同的数据形态统一到了一个类型之下,这正是枚举相比结构体的核心优势:结构体描述"同时拥有"的关系,枚举描述"择一而为"的关系。
构造值:类型名加变体名
枚举值的构造语法需要同时指定类型名和变体名,中间用双冒号::连接。这与元组结构体的构造方式类似,但多了一层"变体"的限定:
// 带数据的变体:变体名后跟小括号,填入对应数据letmsg_write=Message::Write(String::from("hello"));// 具名变体:类似结构体初始化语法,用字段名赋值letmsg_move=Message::Move{x:10,y:30};// 不带数据的变体:没有小括号,没有花括号letmsg_quit=Message::Quit;三种变体的构造语法各不相同,这直接反映了它们携带数据的方式:元组样式的变体用位置参数,结构体样式的变体用字段名,无数据的变体则什么都不带。一旦构造完成,访问变体内的数据需要依赖模式匹配——这正是下一节的主题。在我们正式引入match之前,枚举值中的具体数据尚无法通过点号直接取出,这与结构体字段的直接访问形成了鲜明对比,也是枚举类型在早期学习中需要适应的一个差异点。
值得注意的是,枚举的定义语法本身也内建了类型定义。也就是说,enum IpAddr { V4(String), V6(String) }一次性定义了一个新类型IpAddr,同时定义了它的两个构造入口IpAddr::V4和IpAddr::V6。从语义上看,这两个入口就像两个关联函数,它们的返回值类型都是IpAddr。如果我们要为枚举添加方法,同样可以使用impl块:
implIpAddr{fnis_loopback(&self)->bool{matchself{// 这里使用了 match 表达式,具体语法将在后续章节详细讲解IpAddr::V4(addr)=>addr.starts_with("127."),IpAddr::V6(addr)=>addr=="::1",}}}上面的match表达式尚不在本节展开范围内,但你可以直观地看到:impl块对枚举和结构体一视同仁,方法通过&self借用实例,调用语法同样是instance.method()。枚举与结构体在定义和使用上的对称性,让这两种自定义类型的组合几乎可以建模任何业务领域的数据结构。
回顾本节:枚举通过变体定义了一个类型的有限可能集合;每个变体可以携带不同类型的数据,让枚举从简单的标签升级为携带载荷的形态容器;构造枚举值时使用类型名::变体名的语法,三种变体形态对应三种构造写法。加上impl块对方法的支持,枚举已经具备了与结构体同等的表达能力——而它独有的"形态穷尽"特性,将把模式匹配这一语言特性推向舞台中央。
结构体的内存布局
我们已经熟练地定义和构造结构体了,但一个更底层的问题值得追问:一个结构体实例占据多少字节,字段偏移是否稳定?这直接影响缓存局部性、FFI、序列化和Vec<User>的密度。回答之前必须先区分可观测结果与语言保证。
默认布局:能测量,不等于有契约
默认结构体采用repr(Rust)。Rust Reference 的布局保证要求字段满足对齐且互不重叠,但不保证字段按源码声明顺序排列,也不保证不同编译器版本、目标平台或优化设置下偏移保持不变。编译器可以重排字段。因此,不能根据源码手算偏移,更不能把默认布局直接暴露给 C、磁盘格式或网络协议。
下面这个类型通常占 8 字节,但应以目标平台上的实际测量为准:
structPoint{x:i32,// 4 字节,对齐 4y:i32,// 4 字节,对齐 4}可以用标准库测量大小和对齐:
usestd::mem::{align_of,offset_of,size_of};#[repr(C)]structMixed{a:u8,b:i64,c:u8,}fnmain(){println!("size={}, align={}",size_of::<Mixed>(),align_of::<Mixed>());println!("offsets: a={}, b={}, c={}",offset_of!(Mixed,a),offset_of!(Mixed,b),offset_of!(Mixed,c));}这里显式使用#[repr(C)]后,字段按声明顺序布局,并遵循目标平台 C ABI 的对齐规则;在常见 64 位平台上,a、b、c的偏移通常为 0、8、16,总大小通常为 24。若改成下列顺序,常见结果是 16 字节:
#[repr(C)]structMixed{b:i64,a:u8,c:u8,}这说明字段顺序在要求顺序稳定的表示法中会影响填充,但不能反推出默认repr(Rust)的字段偏移。#[repr(C)]的用途首先是建立 ABI 契约,不是无脑优化内存;#[repr(packed)]会降低对齐并可能产生未对齐访问,不能为了省空间随意使用。
如果布局影响性能,应在目标平台上同时测量size_of、缓存未命中和端到端吞吐,而不是只凭字段大小推断速度。若布局要跨语言或持久化,还必须明确整数宽度、字节序、版本与兼容策略。
枚举布局与 niche 优化
枚举在概念上需要同时表示“当前变体”和“该变体的载荷”,但其物理布局不能简单概括为最大载荷加一个固定整数标签。默认布局由编译器选择,可能使用独立判别值,也可能把某个载荷类型的无效位模式(niche)编码成其他变体。
考虑下面的枚举:
enumShape{Circle(f64),// 8 字节Rectangle{w:u32,h:u32},// 8 字节Nothing,// 0 字节}这个枚举在某个具体目标上的大小可以用size_of::<Shape>()观察,但不要把观察结果当成稳定 ABI。编译器如何放置判别值、载荷和填充属于默认表示的实现细节。
经典例子是Option<&T>:安全引用不能为 null,编译器可以用空指针位模式表示None,因此标准库保证Option<&T>与&T具有相同大小和对齐。类似优化还可能出现在NonZeroUsize等类型上,但不能把这一经验任意推广到所有自定义枚举。
了解枚举的内存布局有助于你理解为什么某些枚举比其他枚举更高效,但它通常不应成为你日常编码中优先考虑的因素。先写清晰的代码,再在性能瓶颈出现时关注布局优化——这是 Rust 社区普遍认可的做法。
小结
repr(Rust)只承诺足以保证类型安全的布局,不承诺字段顺序与跨版本 ABI;repr(C)才用于与 C 布局规则对齐。枚举可能采用判别值,也可能利用 niche。size_of、align_of和offset_of!适合验证目标平台上的事实,但只有语言与 Reference 明确保证的部分才能作为公共接口契约。
至此,我们既掌握了结构体与枚举的值语义,也建立了布局边界:默认布局可以测量,却不能擅自当作协议。下一篇将把重点从“如何构造这些类型”推进到“如何用模式安全地拆解它们”,并进一步讨论部分移动与借用。
设计检查:不变量、可见性与表示法
公开字段会让外部代码直接依赖表示细节;需要维护不变量时,应保持字段私有并通过构造器返回Result<Self, E>。结构体更新语法会逐字段移动非Copy字段,因此旧实例可能发生部分移动。枚举比“布尔值 + 若干可选字段”更能表达互斥状态,因为每个变体只携带该状态合法的数据。只有 FFI、二进制协议或经过测量的性能热点才应选择repr(C)、整数repr、transparent或packed,并为布局假设编写静态断言和跨平台测试。