阶段三 · 复合类型与控制流

结构体与方法

一句话总结

结构体(struct)定义与 Go 相似,差别在:方法写进独立的 impl 块,self 有三种形态(&self / &mut self / self)对应不同的所有权语义,而"构造函数"只是名为 new 的关联函数惯例.
本篇还会认识结构体的三种形态与 #[derive].

定义与实例化

结构体定义和 Go 几乎一样,字段语法略有不同:

struct User {
name: String,
age: u32,
active: bool,
}

let mut u = User {
name: String::from("alice"),
age: 30,
active: true,
};
u.age += 1; // 整个实例需为 mut,才能改任一字段

可变性是整体的,不是字段级的

Rust 没有"只让某个字段可变"的语法--可变性属于整个绑定.要么整个 umut(所有字段都能改),要么都不能改.

这点和 Go 不同(Go 没有这种约束).如果需要"大部分不可变,个别可变",通常意味着该重新设计,或用内部可变性(进阶话题).

字段初始化简写与更新语法

两个高频便利写法.当变量名与字段名相同,可省略冒号(字段初始化简写);从已有实例派生新实例,可用结构体更新语法 ..:

fn build(name: String, age: u32) -> User {
User {
name, // 等价于 name: name(变量名与字段同名,简写)
age,
active: true,
}
}

let u1 = build(String::from("bob"), 25);
let u2 = User {
age: 26,
..u1 // 其余字段从 u1 取(注意:会移动 u1 的非 Copy 字段)
};

.. 更新语法会移动数据

..u1 把 u1 中未显式指定的字段移动到新实例(对非 Copy 字段).所以上例之后 u1.name 不再可用--这是第 03 篇所有权规则的直接体现.

如果只用到 Copy 字段(如 u1.age)则不受影响.

结构体的三种形态

除了常规的具名字段结构体,还有两种特殊形态,实践中都会用到:

// 1. 具名字段结构体(最常用)
struct Point { x: i32, y: i32 }

// 2. 元组结构体:有类型名但字段无名,用 .0 .1 访问
struct Rgb(u8, u8, u8);
let red = Rgb(255, 0, 0);
println!("{}", red.0);

// 3. 单元结构体:没有任何字段,常作类型标记或给它实现 trait
struct Marker;
形态 语法 典型用途
具名字段 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(代表实例本身):

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;
}
}

let mut rect = Rectangle { width: 3, height: 4 };
println!("{}", rect.area()); // 12
rect.scale(2); // 现在是 6 x 8

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 取得实例所有权(消耗它) 转换/销毁场景,调用后原实例失效
impl Rectangle {
// self 取得所有权:把矩形"转换"成另一种东西,原矩形被消耗
fn into_tuple(self) -> (u32, u32) {
(self.width, self.height)
} // 调用 rect.into_tuple() 后,rect 不再可用
}

命名惯例透露所有权

Rust 社区用方法名前缀暗示 self 的形态:as_* 通常借用(&self),into_* 通常取得所有权(self),to_* 通常克隆产出新值.

看到 into_tuple 就该预期"它会消耗掉原对象".这种约定能帮助在不看实现的情况下推断所有权行为.

关联函数:Rust 的"构造函数"

不带 self 参数的函数叫关联函数(associated function),通过 类型::函数名 调用.Rust 没有专门的构造函数语法,惯例是定义一个名为 new 的关联函数:

impl Rectangle {
fn new(width: u32, height: u32) -> Self { // Self 指代当前类型 Rectangle
Rectangle { width, height }
}

fn square(size: u32) -> Self {
Rectangle { width: size, height: size }
}
}

let r = Rectangle::new(3, 4); // :: 调用关联函数
let sq = Rectangle::square(5);

. 与 :: 别用混

实例方法(带 self)用 . 调用:rect.area();关联函数(无 self)用 :: 调用:Rectangle::new(...).

这是新手常见的语法混淆点.String::from,Vec::new 都是关联函数,所以用 ::.

用 derive 自动获得常用能力

结构体默认不能打印,不能比较,不能复制.这些"机械"的能力可以用 #[derive(...)] 一键派生,无需手写:

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

let p = Point { x: 1, y: 2 };
println!("{p:?}"); // Debug:Point { x: 1, y: 2 }(注意 {:?})
let q = p.clone(); // Clone:深拷贝
println!("{}", p == q); // PartialEq:true

最佳实践:给数据结构体常备 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.

动手练习

  1. 基础实现:定义 Rectangle,实现 area(&self),scale(&mut self, f) 和关联函数 new,square.
  2. Newtype 类型安全:用 Newtype 模式定义 UserId(u64)OrderId(u64),写一个只接受 UserId 的函数,验证传 OrderId 会编译失败.
  3. 消耗方法:写一个取得所有权的方法 into_string(self) -> String,调用后尝试再用原实例,观察报错.
  4. derive 验证:给结构体加 #[derive(Debug, Clone, PartialEq)],分别用 {:?} 打印,clone,== 验证.
  5. 更新语法:用字段初始化简写和 .. 更新语法各写一个例子,并解释 .. 之后原实例为何可能不可用.