跳转至

结构体:给相关数据起名字

这一章讲什么: 结构体(struct)——把你程序里"一堆相关的值"打包成一个类型, 并给每个值起名字。读完你会写结构体、实例化它、定义挂在它上面的方法(method), 还会搞懂一个贯穿全书的小机关:#[derive(Debug)] 为什么能让 println! 打印出自定义类型。 本章在链条上的位置:第 4 章管"数据怎么活",这一章开始让你造自己的类型。

1. 顶层全景:主线例子是一次重构

主线例子:算矩形面积。它在本章被重写了三遍,每一遍都暴露上一遍的缺陷:

  第 1 版:area(width, height) 两个散参数
          → 缺陷:看不出两个参数"是一对",调用处全靠猜
  第 2 版:area(dimensions: (u32, u32)) 元组
          → 缺陷:元素没名字,dimensions.0 是宽还是高?看代码的人得背下来
  第 3 版:struct Rectangle { width, height } + area(&rect1)
          → 签名自己会说:算一个 Rectangle,用它的 width 和 height 字段

  图说:三遍重构换来一句话——结构体把"数据的含义"编码进了类型。

2. 定义与实例化

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

let mut user1 = User {
    active: true,
    username: String::from("someusername123"),
    email: String::from("someone@example.com"),
    sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");   // 点号访问/修改字段

三件事值得记住:

  • 字段不必按定义顺序给值,实例化时乱序合法。
  • 整个实例要么都可变,要么都不可变——Rust 不允许只把个别字段标成可变1。
  • 字段初始化简写(field init shorthand):当变量名和字段名相同时,username: username 可以直接写 username。省的是重复,表达的是"这两个就是同一个东西"2。

2.1 结构体更新语法:..user1 与 move

从旧实例造新实例、只改个别字段:

let user2 = User {
    email: String::from("another@example.com"),
    ..user1          // 其余字段从 user1 拷
};

..user1 必须放最后。它的行为是 move,不是 copy:username 是 String,被移进了 user2,从此 user1 整体不可再用;如果新实例只从旧实例拿了 active、sign_in_count 这类 Copy 字段,user1 就仍然有效3。第 4 章的规则在这里原样生效。

2.2 两种特殊结构体

元组结构体(tuple struct):有名字、字段无名字。 struct Color(i32, i32, i32); struct Point(i32, i32, i32); 即使内部三个都是 i32,Color 和 Point 也是两个不同的类型——传 Color 的函数不能收 Point。适用于"想给整个元组一个名字、又不值得给每个字段起名"的场景4。

类单元结构体(unit-like struct):没有字段。 struct AlwaysEqual; 行为类似单元类型 (),适合"要为某个类型实现 trait 但没有数据要存"的场景5。

2.3 一个本章反复撞见的编译错误:结构体里存引用

User 的字段用的是 String 而不是 &str,这是故意的:

结构体里存引用,必须标注生命周期(lifetime),否则报 E0106(missing lifetime specifier)——因为编译器要担保"结构体活得比它引用的数据短",而你得给它推导线头的依据6。

生命周期是第 10 章的题;本章的权宜结论:结构体要么拥有数据(String),要么准备好写生命周期(&'a str)。想现在就动手,照抄编译器报错里的建议 struct User<'a> + &'a str 也能跑,但建议先学完第 10 章再回来。

3. 主线例子走到第三版:结构体版 area

struct Rectangle {
    width: u32,
    height: u32,
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

两处细节都是第 4 章的延伸:参数取 &Rectangle——惯例是函数只借不拿,除非确实需要夺走所有权;访问借用实例的字段不触发 move(字段访问算借用)7。重构的收获书里说得很直白:签名现在精确表达了我们的意思,而且"宽和高是相关的"这件事从"靠人记"变成了"类型写明"8。

4. 派生 trait:让编译器替你实现 Debug

想直接 println!("{rect1}") 打印 Rectangle?两个错依次教你机制:

  1. {} 占位符默认用 Display 格式(给终端用户看的输出)。基本类型有唯一的"给人看"的样子,所以默认实现了 Display;结构体"想怎么显示"没有唯一答案(带不带逗号?显示哪些字段?),Rust 拒绝替你猜,所以没有现成 Display9。
  2. 报错提示换 {:?}——Debug 格式,给开发者调试看的。换完还报错:Rectangle 没实现 Debug。再按提示加 #[derive(Debug)],编译通过,输出 rect1 is Rectangle { width: 30, height: 50 }10。

#[derive(Debug)] 是一个属性(attribute):标注在类型定义前,编译器自动为该类型生成一个默认的 Debug 实现——这就是第 2 章、本章反复出现的"derive"机关。它能派生的 trait 不止 Debug,附录 C 有全表(Debug、Clone、PartialEq……)11。

配套的调试工具还有 dbg! 宏:拿走一个表达式的所有权、打印出它所在的文件名和行号以及求值结果,再把所有权还回来。输出走 stderr(不是 println! 的 stdout)12。例:width: dbg!(30 * scale) 会打印 [src/main.rs:10:16] 30 * scale = 60,width 字段照样得到 60——所有权绕了一圈回来了13。

5. 方法:定义在类型上下文里的函数

方法与函数的区别只有一个:方法定义在某个类型的上下文里,第一个参数永远是 self(调用它的那个实例)。 方法住在 impl 块 里:

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

&self 是 self: &Self 的简写;Self 是 impl 所指类型的别名。三个选择对应第 4 章的三种借用:&self(只读,最常见)、&mut self(要改)、self(夺走实例——少见,用于"把 self 变成别的东西、之后禁止再用原实例"的转换)14。

书给了方法存在的理由:组织——"这个类型的实例能做什么"全部收在一个 impl 块里,使用者不用在库代码里四处搜15。方法可以和字段同名(fn width(&self) -> bool),带括号是方法、不带是字段;这种"只返回字段值"的方法叫 getter,Rust 不会自动生成,手写它的意义在于:字段设私有、方法设公开,实现只读访问16。

没有 -> 运算符:C/C++ 里指针对象调方法要用 ->;Rust 用自动引用与解引用——p1.distance(&p2) 和 (&p1).distance(&p2) 等价,编译器按方法签名自动补 &、&mut 或 *。书说这是"所有权在实践里好用"的重要一环17。

关联函数(associated function):impl 块里不以 self 为首的函数。 String::from、Rectangle::square(size) 都是;常用作构造函数,new 只是约定俗成的名字、不是语言内建18。调用用 ::。

一个类型可以有多个 impl 块——本章看着没必要,第 10 章讲泛型时会用到19。

6. 作者的判断与证据

  • "结构体字段用 owned String 是 deliberate choice"——设计主张,理由是实例应当拥有全部数据、和数据同寿6。
  • "Rust 拒绝替结构体猜 Display 格式"——设计决策,书里把理由写成了反问句(要不要逗号?哪些字段?),属于"不猜"哲学9。
  • "函数只借不拿"——惯例,理由是保住调用方的所有权7。

判断(我们的,不是书里的): 本章看似教语法,真正的训练是"用重构学判断": 同一个面积函数写三遍,每遍的缺陷都精确对应一种类型表达能力(散变量 < 元组 < 命名结构体)。 这个"缺陷驱动"的讲法值得复用——它示范了什么时候该造新类型: 当两个值"总是一起出现、一起变化"而类型系统还没把它们绑在一起时。 如果错,会错在: 过度打包同样有害——为只在一个函数里出现一次的一对值造结构体是负担; 判据是"这组数据是否被多处传递并依赖彼此的一致性"。

7. 边界与局限

  • 生命周期在结构体里只以报错形式出现,机制完全留给第 10 章。
  • getter 与可见性(public/private)只给了预告,第 7 章讲。
  • Display 的自定义实现(impl fmt::Display)本章没教,只给了 Debug 的 derive 路线。

8. 可带走的

  1. 结构体 = 命名打包;实例化乱序合法;可变性属于整个实例。
  2. 字段初始化简写:变量与字段同名时只写一遍。
  3. ..old 更新语法在最后,行为是 move——被拿走的字段所属的旧实例部分失效。
  4. 元组结构体:struct Name(T1, T2); 名字即类型,同构也互斥。
  5. 结构体存引用必须写生命周期(E0106);在此之前,字段用 owned 类型。
  6. #[derive(Debug)] + {:?}/{:#?} 打印自定义类型;dbg! 打表达式值(走 stderr)。
  7. 方法第一个参数 self:&self 只读、&mut self 改、self 收走。
  8. 方法调用自动加 &/&mut/*,没有 ->。
  9. 关联函数 = impl 里无 self 的函数,Type::name() 调用,常作构造函数。

9. 原文地图

主题 原书章 原文位置
实例可变性与字段修改 Chapter 5: Using Structs to Structure Related Data text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:76(搜「entire instance must be mutable」)
字段初始化简写 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:101(搜「Field Init Shorthand」)
更新语法是 move 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:166(搜「uses = like an assignment」)
元组结构体 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:177(搜「Tuple Structs」)
类单元结构体 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:207(搜「Unit-Like Structs」)
结构体存引用要 lifetime 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:1(搜「STRUCT」)
三遍重构 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:294(搜「An Example Program Using Structs」)
Display 不替你猜 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:444(搜「Due to this ambiguity」)
derive(Debug) 与 dbg! 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:467(搜「outer attribute」) · text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:504(搜「dbg! macro」)
方法语法与 Self 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:584(搜「impl Rectangle」) · text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:614(搜「alias for the type」)
自动引用解引用 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:674(搜「THE ->」)
关联函数与多个 impl 同上 text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:762(搜「Associated Functions」) · text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:793(搜「Multiple impl Blocks」)


  1. 出处:「Chapter 5: Using Structs to Structure Related Data」第 76 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:76,搜「entire instance must be mutable」)。 ↩

  2. 出处:「Chapter 5: Using Structs to Structure Related Data」第 101 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:101,搜「Field Init Shorthand」)。 ↩

  3. 出处:「Chapter 5: Using Structs to Structure Related Data」第 166 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:166,搜「uses = like an assignment」)。 ↩

  4. 出处:「Chapter 5: Using Structs to Structure Related Data」第 177 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:177,搜「Tuple Structs」)与第 196 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:196,搜「different types」)。 ↩

  5. 出处:「Chapter 5: Using Structs to Structure Related Data」第 207 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:207,搜「Unit-Like Structs」)。 ↩

  6. 出处:「Chapter 5: Using Structs to Structure Related Data」第 234 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:234,搜「deliberate choice」)。 ↩↩

  7. 出处:「Chapter 5: Using Structs to Structure Related Data」第 397 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:397,搜「immutable borrow」)与第 404 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:404,搜「not move the field values」)。 ↩↩

  8. 出处:「Chapter 5: Using Structs to Structure Related Data」第 405 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:405,搜「says exactly what we mean」)。 ↩

  9. 出处:「Chapter 5: Using Structs to Structure Related Data」第 444 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:444,搜「Due to this ambiguity」)。 ↩↩

  10. 出处:「Chapter 5: Using Structs to Structure Related Data」第 449 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:449,搜「not implemented for Rectangle」)与第 470 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:470,搜「derive(Debug)」)。 ↩

  11. 出处:「Chapter 5: Using Structs to Structure Related Data」第 551 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:551,搜「number of traits」)。 ↩

  12. 出处:「Chapter 5: Using Structs to Structure Related Data」第 504 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:504,搜「dbg! macro」)与第 509 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:509,搜「standard error」)。 ↩

  13. 出处:「Chapter 5: Using Structs to Structure Related Data」第 539 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:539,搜「30 * scale = 60」)。 ↩

  14. 出处:「Chapter 5: Using Structs to Structure Related Data」第 614 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:614,搜「alias for the type」)与第 504 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:504,搜「takes ownership of」)。 ↩

  15. 出处:「Chapter 5: Using Structs to Structure Related Data」第 632 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:632,搜「for organization」)。 ↩

  16. 出处:「Chapter 5: Using Structs to Structure Related Data」第 667 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:667,搜「called getters」)。 ↩

  17. 出处:「Chapter 5: Using Structs to Structure Related Data」第 681 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:681,搜「automatic referencing and dereferencing」)。 ↩

  18. 出处:「Chapter 5: Using Structs to Structure Related Data」第 769 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:769,搜「often used for」)。 ↩

  19. 出处:「Chapter 5: Using Structs to Structure Related Data」第 793 段(text/12-ch05-chapter-5-using-structs-to-structure-related-dat.txt:793,搜「Multiple impl Blocks」)。 ↩