资讯动态

Rust 编程语言(The Rust Programming Language):用结构体 Struct 组织相关联的数据

发布时间:2026/10/3 17:38:36 来源:尧图企业网站定制
教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载结构体struct是 Rust 中用于把多个相关联的值打包成一个有意义整体、并给每个部分命名的自定义数据类型。本文基于《The Rust Programming Language》本项目仓库中的 book 开源书籍第 5 章内容系统讲解结构体的定义与实例化、字段初始化简写、结构体更新语法、元组结构体、类单元结构体、派生 traitDebug、方法method与关联函数associated function并辅以仓库中的可运行代码清单listings/ch05-using-structs-to-structure-related-data/作为实操佐证。读完本文你将掌握如何在 Rust 中创建自定义类型、通过impl块为其绑定行为并能够理解结构体在所有权与借用体系下的正确用法。结构体与元组的对比为什么需要结构体结构体与元组tuple类似都可以容纳多个不同类型的相关值。但二者的关键区别在于元组的元素没有名字只能依赖位置索引如dimensions.0、dimensions.1来访问而结构体为每一块数据都命名称为字段 field访问时无需关心声明顺序代码语义一目了然。也就是说结构体定义相当于类型的一份通用模板而实例instance则是用具体数据填充该模板后得到的类型值。定义与实例化结构体定义结构体使用struct关键字结构体名称应能概括被组合数据的含义随后在大括号内声明各字段的名称与类型。以仓库中 listing-05-01/src/main.rs 的User结构体为例struct User { active: bool, username: String, email: String, sign_in_count: u64, }创建实例创建实例时写出结构体名后跟包含key: value键值对的大括号。字段无需按声明顺序书写fn main() { let user1 User { active: true, username: String::from(someusername123), email: String::from(someoneexample.com), sign_in_count: 1, }; }访问与修改字段使用点号dot notation访问字段例如user1.email。若实例可变let mut还可以用点号配合赋值修改某个字段fn main() { let mut user1 User { active: true, username: String::from(someusername123), email: String::from(someoneexample.com), sign_in_count: 1, }; user1.email String::from(anotheremailexample.com); }注意整个实例必须可变Rust 不允许只把个别字段标记为可变。另外与任何表达式一样可以把新建实例作为函数体的最后一个表达式来隐式返回该实例。字段初始化简写Field Init Shorthand当函数参数名与结构体字段名完全一致时可以直接写字段名而不必重复字段: 参数的形式fn build_user(email: String, username: String) - User { User { active: true, username, email, sign_in_count: 1, } }这里username与email参数名和字段名相同因此只需写username、email即可行为与username: username、email: email完全一致。结构体更新语法Struct Update Syntax当需要基于某个已有实例创建新实例、仅改动部分字段时可以用..语法把其余字段从给定实例中补齐。..user1必须放在最后但其余字段可以任意顺序、任意数量地显式指定fn main() { // --snip-- let user2 User { email: String::from(anotherexample.com), ..user1 }; }值得特别强调的是结构体更新语法使用类似赋值本质上是*移动move*数据。在上例中user1的username字段一个String被移动进了user2因此创建user2之后不能再使用user1的该字段。如果user2的email和username都使用全新的String只从user1借用active与sign_in_count二者均实现了Copytrait那么user1在创建user2后依然有效此例中user1.email也仍可使用因为它的值没有被移走。元组结构体Tuple Structs元组结构体拥有结构体名称带来的类型含义但字段没有名字只有类型。当不想为每个字段命名、又要让整个元组成为独立类型时非常有用struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black Color(0, 0, 0); let origin Point(0, 0, 0); }关键点black与origin是不同类型——每个struct定义都是独立类型即使内部字段类型完全相同比如都是三个i32接受Color的函数也不能接收Point。与元组类似元组结构体可以解构为各个部分也可以用.加索引访问单个值但与元组不同的是解构时必须写出结构体类型名例如let Point(x, y, z) origin;。类单元结构体Unit-Like Structs不含任何字段的结构体称为类单元结构体行为类似()单元类型。当需要在某个类型上实现 trait 而又无需存储数据时例如测试中需要永远相等的已知结果类型可以用它struct AlwaysEqual; fn main() { let subject AlwaysEqual; }定义时只需struct关键字、名称和分号实例化同样不需要大括号或圆括号。仓库中的 no-listing-04-unit-like-structs/src/main.rs 可直接运行验证。结构体的数据所有权为什么用 String 而不是 str在User定义中使用拥有所有权的String而非字符串切片str是有意为之我们希望每个实例拥有自己的全部数据并且数据在结构体存续期间始终有效。如果试图在结构体中存放引用而不标注生命周期编译会失败。仓库中的 no-listing-02-reference-in-struct/src/main.rs 展示了这段无法编译的代码struct User { active: bool, username: str, email: str, sign_in_count: u64, }对应的编译错误记录在 no-listing-02-reference-in-struct/output.txt 中error[E0106]: missing lifetime specifier -- src/main.rs:3:15 | 3 | username: str, | ^ expected named lifetime parameter编译器会提示引入具名生命周期参数例如struct Usera配合username: a str。生命周期确保被引用的数据与结构体本身存活时间一致相关内容将在本书第 10 章深入讲解在此之前遇到此类报错时用String等拥有类型替代str引用即可。实战案例用结构体重构矩形面积程序为了理解何时该用结构体我们以计算矩形面积为例从裸变量逐步重构到结构体与方法的完整形态。先用 Cargo 新建名为rectangles的二进制项目并在src/main.rs中编写代码。第一阶段两个独立变量fn main() { let width1 30; let height1 50; println!( The area of the rectangle is {} square pixels., area(width1, height1) ); } fn area(width: u32, height: u32) - u32 { width * height }运行cargo run输出The area of the rectangle is 1500 square pixels.。问题在于area函数有两个参数程序中没有任何地方表明这两个参数是关联的可读性差。第二阶段用元组重构fn main() { let rect1 (30, 50); println!( The area of the rectangle is {} square pixels., area(rect1) ); } fn area(dimensions: (u32, u32)) - u32 { dimensions.0 * dimensions.1 }元组让参数个数变成一个但元素没有名字必须记住width是索引0、height是索引1。对面积计算影响不大但若要在屏幕上绘制矩形混用宽高就很容易出错——代码没有传达数据含义。第三阶段用结构体重构struct Rectangle { width: u32, height: u32, } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!( The area of the rectangle is {} square pixels., area(rect1) ); } fn area(rectangle: Rectangle) - u32 { rectangle.width * rectangle.height }这里area接收的是Rectangle——对结构体的不可变借用借用的原理详见第 4 章这样main保留所有权之后仍可使用rect1。访问被借用结构体的字段并不会移动字段值这也是借用结构体在代码中常见的原因。函数签名现在精确表达了语义计算一个Rectangle的面积。完整的可运行代码见 listing-05-10/src/main.rs。用派生 trait 添加功能Debug 与 dbg!调试时往往希望打印出结构体实例及其所有字段值但直接println!(rect1 is {rect1})会编译失败error[E0277]: Rectangle doesnt implement std::fmt::Displayprintln!的{}占位符默认使用Display格式面向终端用户的输出。基本类型只有一种直观展示方式故默认实现Display而结构体的展示方式存在多种可能是否带逗号、是否带大括号、展示哪些字段Rust 不做猜测因此结构体没有内置Display实现。编译器会给出提示可以改用{:?}或{:#?}。把打印语句改为println!(rect1 is {rect1:?})后又会出现error[E0277]: Rectangle doesnt implement Debug help: the trait Debug is not implemented for Rectangle note: add #[derive(Debug)] to Rectangle or manually impl Debug for RectangleDebug格式面向开发者调试。Rust 内置了打印调试信息的能力但需要显式开启——在结构体定义前添加外部属性#[derive(Debug)]#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!(rect1 is {rect1:?}); }运行后输出rect1 is Rectangle { width: 30, height: 50 }当结构体字段较多时用{:#?}pretty-print可获得更易读的多行输出rect1 is Rectangle { width: 30, height: 50, }使用 dbg! 宏另一种以Debug格式打印的方式是dbg!宏。与println!接收引用、输出到 stdout不同dbg!会取得表达式所有权打印宏调用所在的文件与行号及表达式的值然后把所有权交还且输出到标准错误流stderrstderr与stdout的区别在本书第 12 章讲解。仓库中的 no-listing-05-dbg-macro/src/main.rs 是完整示例#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale 2; let rect1 Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(rect1); }运行输出[src/main.rs:10:16] 30 * scale 60 [src/main.rs:14:5] rect1 Rectangle { width: 60, height: 50, }因为dbg!会返回表达式值width字段正常获得60对rect1则传入引用避免宏拿走所有权。除Debug外derive属性还支持若干其他 trait完整列表见附录 C自定义实现与自定义 trait 的写法在第 10 章介绍。方法Method方法method与函数类似用fn声明、有参数与返回值、包含可执行代码。区别在于方法定义在结构体或枚举、trait 对象的上下文中且第一个参数永远是self代表调用该方法的结构体实例。方法语法与 impl 块将area函数改造成Rectangle的方法需要使用implimplementation块#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(self) - u32 { self.width * self.height } } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!( The area of the rectangle is {} square pixels., rect1.area() ); }在impl Rectangle块内的所有内容都与Rectangle类型相关联。调用时使用方法语法实例后加点号、方法名、圆括号与参数rect1.area()。self是self: Self的简写——在impl块中Self是当前类型的别名。方法第一个参数必须是类型为Self、名为self的参数Rust 允许只用self简写前面的表示借用方式。方法可以取得self所有权、不可变借用self本例如是或可变借用mut self。若方法需要改写实例本身就用mut self直接以self取得所有权较为少见通常用于把self变换成别的类型、并阻止调用方继续使用原实例的场景。完整代码见 listing-05-13/src/main.rs。使用方法而非函数的主要理由是组织性把某个类型实例能做的所有事情集中在同一impl块内使用者无需在代码各处寻找该类型的能力。方法名与字段同名getter方法可以刻意与字段同名。Rust 根据是否带圆括号区分二者rect1.width()调用方法rect1.width访问字段。仓库中的 no-listing-06-method-field-interaction/src/main.rs 展示了这种写法impl Rectangle { fn width(self) - bool { self.width 0 } } fn main() { let rect1 Rectangle { width: 30, height: 50, }; if rect1.width() { println!(The rectangle has a nonzero width; it is {}, rect1.width); } }这种只返回字段值、不做其他事情的方法称为getter。Rust 不像某些语言那样为结构体字段自动生成 getter手写 getter 的价值在于可以把字段设为私有、方法设为公有从而在类型公共 API 中提供对该字段的只读访问公有/私有的控制见第 7 章。为什么没有 - 运算符C/C 中调用对象方法与指针方法分别使用.与-后者相当于先解引用(*object).something()。Rust 没有-运算符取而代之的是自动引用与解引用automatic referencing and dereferencing调用object.something()时Rust 会自动补上、mut或*使object匹配方法的签名。例如下面两种写法等价p1.distance(p2); (p1).distance(p2);之所以可行是因为方法有明确的接收者——self的类型。根据接收者与方法名Rust 能确定方法是读取self、修改mut self还是消费self。方法接收者隐式借用是让所有权在实际使用中保持舒适ergonomic的重要因素。带多个参数的方法can_hold方法在self之后可以带多个参数用法与函数参数一致。下面实现can_hold判断一个Rectangle能否完全容纳另一个impl Rectangle { fn area(self) - u32 { self.width * self.height } fn can_hold(self, other: Rectangle) - bool { self.width other.width self.height other.height } } fn main() { let rect1 Rectangle { width: 30, height: 50, }; let rect2 Rectangle { width: 10, height: 40, }; let rect3 Rectangle { width: 60, height: 45, }; println!(Can rect1 hold rect2? {}, rect1.can_hold(rect2)); println!(Can rect1 hold rect3? {}, rect1.can_hold(rect3)); }参数rect2是对Rectangle的不可变借用方法只需读取rect2且main在调用后仍要使用它。输出为Can rect1 hold rect2? true Can rect1 hold rect3? false该示例在仓库中的完整可运行版本见 listing-05-15/src/main.rs。关联函数Associated Functionsimpl块内定义的所有函数都称为关联函数因为它们与impl后面的类型相关联。不以self作为第一个参数的关联函数不是方法它们不需要类型实例即可调用——前面用过的String::from就是典型例子。不以self开头的关联函数常用于构造函数返回该结构体的新实例。这类函数习惯命名为new但new并非语言内置的特殊名称impl Rectangle { fn square(size: u32) - Self { Self { width: size, height: size, } } }返回类型与函数体中的Self都是impl关键字后类型的别名此处即Rectangle。调用时使用::语法并冠以结构体名let sq Rectangle::square(3);。::语法既用于关联函数也用于模块命名空间见第 7 章。仓库中的 no-listing-03-associated-functions/src/main.rs 提供了可运行的完整版本。多个 impl 块每个结构体允许拥有多个impl块。例如把area与can_hold分别放进两个impl Rectangle块是完全合法的impl Rectangle { fn area(self) - u32 { self.width * self.height } } impl Rectangle { fn can_hold(self, other: Rectangle) - bool { self.width other.width self.height other.height } }当前示例没有拆分的必要但这是有效语法第 10 章讨论泛型与 trait 时会看到多impl块的实用场景。本章配套源码与延伸阅读本章全部可编译/可复现的代码清单位于仓库的listings/ch05-using-structs-to-structure-related-data/目录覆盖定义listing-05-01、实例化listing-05-02、字段修改listing-05-03、简写listing-05-05、更新语法listing-05-07、元组结构体no-listing-01、类单元结构体no-listing-04、生命周期报错no-listing-02、派生 trait 调试listing-05-12、no-listing-05-dbg-macro、output-only-01-debug、output-only-02-pretty-debug以及方法与关联函数listing-05-13 至 listing-05-16。本仓库的src/目录保留了本书主版本对应章节源文件ch05-01-defining-structs.md、ch05-02-example-structs.md 与 ch05-03-method-syntax.md可对照阅读。直接使用println!({:?}, x)遇到编译错误时运行rustc --explain E0277可获取Debug/Display的详细解释。小结结构体让你能创建对业务领域有意义的自定义类型把相关联的数据聚合在一起并为每块数据命名使代码清晰可读。在impl块中可以定义与类型关联的函数其中方法第一个参数为self规定了结构体实例的行为。结构体并非创建自定义类型的唯一途径——接下来本书将进入第 6 章介绍枚举enum这一同为自定义类型基石的语言特性配合模式匹配发挥更强大的类型检查能力。赞分享教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载相关推荐如何为老旧Intel Mac安装最新macOSOpenCore Legacy Patcher终极升级指南如何为老旧Intel Mac安装最新macOSOpenCore Legacy Patcher终极升级指南 OpenCore Legacy Patcher是一款操作系统固件驱动开发The Rust Programming Language如何编写高效Rust函数的7个技巧The Rust Programming Language如何编写高效Rust函数的7个技巧 Rust作为一门现代系统编程语言其函数设计哲学强调安全、性能和教程文档终极指南如何简单快速解决Cursor试用限制问题终极指南如何简单快速解决Cursor试用限制问题 你是否曾经遇到过这样的情况刚刚开始享受Cursor AI代码编辑器的流畅体验突然弹出一条令人沮丧的提示开发工具CLI上一篇Diffusers 实战指南Stable Diffusion 推理提速、显存优化与画质提升下一篇PaddleOCR 手写 OCR 数据集实战指南CASIA-HWDB 与 NIST19 的获取、格式适配与训练方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑