一句话总结
结构体(struct)定义与 Go 相似,差别在:方法写进独立的 impl 块,self 有三种形态(&self / &mut self / self)对应不同的所有权语义,而"构造函数"只是名为 new 的关联函数惯例.
本篇还会认识结构体的三种形态与 #[derive].
定义与实例化
结构体定义和 Go 几乎一样,字段语法略有不同:
|
可变性是整体的,不是字段级的
Rust 没有"只让某个字段可变"的语法--可变性属于整个绑定.要么整个 u 是 mut(所有字段都能改),要么都不能改.
这点和 Go 不同(Go 没有这种约束).如果需要"大部分不可变,个别可变",通常意味着该重新设计,或用内部可变性(进阶话题).
字段初始化简写与更新语法
两个高频便利写法.当变量名与字段名相同,可省略冒号(字段初始化简写);从已有实例派生新实例,可用结构体更新语法 ..:
|
.. 更新语法会移动数据
..u1 把 u1 中未显式指定的字段移动到新实例(对非 Copy 字段).所以上例之后 u1.name 不再可用--这是第 03 篇所有权规则的直接体现.
如果只用到 Copy 字段(如 u1.age)则不受影响.
结构体的三种形态
除了常规的具名字段结构体,还有两种特殊形态,实践中都会用到:
|
| 形态 | 语法 | 典型用途 |
|---|---|---|
| 具名字段 | struct S { a: T } |
绝大多数数据建模 |
| 元组结构体 | struct S(T, U) |
给一组值起类型名,如 Meters(f64) 的"新类型"包装 |
| 单元结构体 | struct S; |
无数据的类型标记,常用于实现 trait |
新类型模式(Newtype):元组结构体的妙用
用单字段元组结构体包装一个类型,能创造出有区分度的新类型--比如 struct UserId(u64) 和 struct ProductId(u64),二者底层都是 u64,但类型系统会阻止把 UserId 误传给需要 ProductId 的地方.
Go 里靠 type UserId uint64 实现类似效果,Rust 的 Newtype 更进一步(还能为它单独实现 trait).
方法:写在 impl 块里
Rust 的方法不写在结构体定义内部,而是放进独立的 impl 块.方法的第一个参数是 self(代表实例本身):
|
impl Rectangle { fn area(&self) } 对应 Go 的 func (r *Rectangle) Area().
&self 约等于 Go 的指针接收者 *Rectangle(借用,不复制).区别是 Rust 把方法集中在 impl 块里,而非散落在各处的接收者函数.
self 的三种形态:对应三种所有权
这是 Rust 方法的精髓,直接呼应第 03 篇所有权系统.第一个参数怎么写,决定了方法如何对待实例:
| 写法 | 含义 | 何时用 |
|---|---|---|
&self |
不可变借用实例(最常见) | 只读取数据,如 area,各种 getter |
&mut self |
可变借用实例 | 需要修改字段,如 scale,push |
self |
取得实例所有权(消耗它) | 转换/销毁场景,调用后原实例失效 |
|
命名惯例透露所有权
Rust 社区用方法名前缀暗示 self 的形态:as_* 通常借用(&self),into_* 通常取得所有权(self),to_* 通常克隆产出新值.
看到 into_tuple 就该预期"它会消耗掉原对象".这种约定能帮助在不看实现的情况下推断所有权行为.
关联函数:Rust 的"构造函数"
不带 self 参数的函数叫关联函数(associated function),通过 类型::函数名 调用.Rust 没有专门的构造函数语法,惯例是定义一个名为 new 的关联函数:
|
. 与 :: 别用混
实例方法(带 self)用 . 调用:rect.area();关联函数(无 self)用 :: 调用:Rectangle::new(...).
这是新手常见的语法混淆点.String::from,Vec::new 都是关联函数,所以用 ::.
用 derive 自动获得常用能力
结构体默认不能打印,不能比较,不能复制.这些"机械"的能力可以用 #[derive(...)] 一键派生,无需手写:
|
最佳实践:给数据结构体常备 Debug
几乎所有数据类的结构体都该 #[derive(Debug)]--它让能用 {:?} 打印调试,也是许多库和测试断言的前提.这是 Rust 项目里最常见的派生.
Clone,PartialEq 按需添加.derive 的完整机制(它其实是过程宏)和更多可派生 trait,留到第 12 篇特征再展开.
快速回顾
- 三种形态:具名字段(主力),元组结构体(Newtype 模式),单元结构体(类型标记).
- 可变性整体生效:实例为
mut才能改字段;无字段级可变性. - 便利语法:字段初始化简写,
..更新语法(会移动非 Copy 字段). - 方法在 impl 块:
&self(借用,最常用)/&mut self(改)/self(消耗),对应三种所有权. - 关联函数:无 self,用
::调用;惯例用new当构造函数,Self指代当前类型. - derive:一键派生
Debug/Clone/PartialEq等;数据结构体常备Debug.
动手练习
- 基础实现:定义
Rectangle,实现area(&self),scale(&mut self, f)和关联函数new,square. - Newtype 类型安全:用 Newtype 模式定义
UserId(u64)和OrderId(u64),写一个只接受UserId的函数,验证传OrderId会编译失败. - 消耗方法:写一个取得所有权的方法
into_string(self) -> String,调用后尝试再用原实例,观察报错. - derive 验证:给结构体加
#[derive(Debug, Clone, PartialEq)],分别用{:?}打印,clone,==验证. - 更新语法:用字段初始化简写和
..更新语法各写一个例子,并解释..之后原实例为何可能不可用.