ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Rust 结构体(Struct)完全指南:使用 struct 组织相关联的数据

Rust 结构体(Struct)完全指南:使用 struct 组织相关联的数据 教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载本指南以《The Rust Programming Language》第 5 章仓库路径 src/ch05-00-structs.md 及其三个分节为主体系统讲解 Rust 中自定义数据类型 struct结构体的定义、实例化、所有权语义、派生 trait 与方法绑定。读完本指南你将能够使用结构体为业务领域建模、用结构体更新语法高效复制实例、通过impl块为类型附加方法与关联函数并利用Debug派生 trait 完成调试输出从而充分发挥 Rust 编译期类型检查的优势。结构体是什么给一组相关联的数据起名字结构体struct全称 structure是一种自定义数据类型用于把多个相互关联、共同构成一个有意义整体的值打包在一起并分别命名。如果你熟悉面向对象语言可以把结构体类比为对象的数据属性data attributes。与元组相比相同点二者都能容纳多个相关联的值且这些值的类型可以各不相同。不同点结构体为每一块数据命名语义一目了然而元组只能靠下标访问必须依赖数据的排列顺序。在第 3 章的“元组类型”一节src/ch03-02-data-types.md中你已经见过元组。本指南将反复对比元组与结构体说明什么场景下结构体是更好的数据分组方式。结构体与第 6 章要讲的枚举enum共同构成了在程序领域内创建新类型的基础设施是充分享受 Rust 编译期类型检查的两块基石。定义与实例化结构体使用struct关键字定义定义结构体时先写struct关键字再写整个结构体的名字名字应当能描述被组合数据的含义然后在大括号内声明各字段field的名字与类型。下面的示例定义了一个存放用户账户信息的User结构体见 listings/ch05-using-structs-to-structure-related-data/listing-05-01/src/main.rsstruct User { active: bool, username: String, email: String, sign_in_count: u64, }用字段键值对创建实例定义好结构体后通过为每个字段指定具体值来创建实例instance。语法是写结构体名后跟大括号大括号内是key: value键值对键是字段名值是要存入的数据。字段的书写顺序不必与定义时一致——结构体定义是类型的通用模板实例则用具体数据填充模板得到该类型的一个值let user1 User { active: true, username: String::from(someusername123), email: String::from(someoneexample.com), sign_in_count: 1, };对应完整代码见 listings/ch05-using-structs-to-structure-related-data/listing-05-02/src/main.rs。点号访问与字段修改要从结构体取值使用点号.记法user1.email即访问该用户的邮箱。若实例是可变的还可以对特定字段赋值修改见 listings/ch05-using-structs-to-structure-related-data/listing-05-03/src/main.rslet mut user1 User { active: true, username: String::from(someusername123), email: String::from(someoneexample.com), sign_in_count: 1, }; user1.email String::from(anotheremailexample.com);需要注意整个实例必须声明为mut才可修改Rust 不允许只把某些字段标记为可变。与任何表达式一样你也可以把新实例的构造作为函数体最后一个表达式从而隐式返回该新实例。构造函数与字段初始化简写Field Init Shorthand下面这个build_user函数接收邮箱和用户名返回一个User实例其中active为true、sign_in_count为1见 listings/ch05-using-structs-to-structure-related-data/listing-05-04/src/main.rsfn build_user(email: String, username: String) - User { User { active: true, username: username, email: email, sign_in_count: 1, } }把函数参数命名为与字段相同的名字虽然语义清晰但username: username、email: email的重复书写颇为繁琐字段越多越啰嗦。好在 Rust 提供了字段初始化简写field init shorthand当参数名与字段名完全一致时只需写一次名字即可行为与完整写法完全相同见 listings/ch05-using-structs-to-structure-related-data/listing-05-05/src/main.rsfn build_user(email: String, username: String) - User { User { active: true, username, email, sign_in_count: 1, } }因为email字段和email参数同名所以只写email就等价于email: email。结构体更新语法Struct Update Syntax与所有权移动实际开发中常需要创建“与另一个实例大部分相同、仅改动少数字段”的新实例。先看不用更新语法的写法见 listings/ch05-using-structs-to-structure-related-data/listing-05-06/src/main.rslet user2 User { active: user1.active, username: user1.username, email: String::from(anotherexample.com), sign_in_count: user1.sign_in_count, };使用结构体更新语法可大幅精简见 listings/ch05-using-structs-to-structure-related-data/listing-05-07/src/main.rslet user2 User { email: String::from(anotherexample.com), ..user1 };..user1表示未显式设置的其余字段其值与给定实例user1的对应字段相同。语法要点..user1必须放在最后指明其余字段从user1取值显式设置的字段可以按任意顺序书写不必与结构体定义中的字段顺序一致。所有权语义是此处必须注意的关键点结构体更新语法使用如同赋值一样会移动数据。上面示例创建user2后user1便不能再使用因为user1的username字段String被移动进了user2。但如果给user2的email和username都提供全新的String值只从user1复制active和sign_in_count那么user1在创建user2后仍然有效——因为bool和u64实现了Copytrait属于第 4 章“仅栈上数据Copy”一节src/ch04-01-what-is-ownership.md讨论的复制行为。本例中user1.email也仍可使用因为它的值没有被移走。关于移动语义的完整机制可回顾第 4 章“变量与数据交互移动”一节src/ch04-01-what-is-ownership.md。元组结构体Tuple Structs只有类型没有字段名Rust 还支持外观类似元组、但带结构体名的元组结构体tuple struct。它保留了结构体名带来的类型含义却不为字段命名只有字段类型。当你希望给整个元组起名、使之与其他元组成为不同类型而像普通结构体那样逐字段命名又显得冗长时元组结构体正合适。定义元组结构体struct关键字 结构体名 括号内的字段类型列表见 listings/ch05-using-structs-to-structure-related-data/no-listing-01-tuple-structs/src/main.rsstruct 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是不同类型的值因为它们是不同元组结构体的实例。每个自定义结构体都是独立类型——即使字段类型完全相同这里都是三个i32接受Color参数的函数也不能接收Point。其余行为与元组类似可以解构为独立变量也可以用.加下标访问单个值origin.0。与元组的区别在于解构时必须写出结构体类型名let Point(x, y, z) origin;可把origin的三个值分别绑定到变量x、y、z。类单元结构体Unit-Like Structs零字段结构体还可以定义没有任何字段的结构体称为类单元结构体unit-like struct因为它与单元类型()行为相似()在第 3 章“元组类型”一节中介绍过。当你需要在某个类型上实现 trait、但又无需在该类型中存储任何数据时类单元结构体非常有用。下面是名为AlwaysEqual的示例见 listings/ch05-using-structs-to-structure-related-data/no-listing-04-unit-like-structs/src/main.rsstruct AlwaysEqual; fn main() { let subject AlwaysEqual; }定义时用struct关键字 名字 分号即可既不需要大括号也不需要括号实例化同样只需名字。设想后续实现让每个AlwaysEqual实例与任何其他类型的实例都相等例如为测试提供已知结果这样的行为不需要任何数据就能实现。第 10 章将介绍如何在包括类单元结构体在内的任何类型上定义并实现 trait。结构体数据的所有权为什么用String而非str在User的定义中我们特意使用自有类型String而非字符串切片str这是有意为之希望每个实例都拥有其全部数据且数据在结构体整个生命周期内始终有效。结构体也可以存储由其他对象借用的引用但这需要用到生命周期lifetimes——第 10 章的主题。生命周期确保结构体引用的数据在结构体存活期间始终有效。如果不指定生命周期就在结构体中存引用编译将失败。例如下面代码无法通过编译struct User { active: bool, username: str, email: str, sign_in_count: u64, } fn main() { let user1 User { active: true, username: someusername123, email: someoneexample.com, sign_in_count: 1, }; }编译器会报error[E0106]: missing lifetime specifier并给出建议$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier -- src/main.rs:3:15 | 3 | username: str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct Usera { 2 | active: bool, 3 ~ username: a str, |完整的报错输出记录在 listings/ch05-using-structs-to-structure-related-data/no-listing-02-reference-in-struct/output.txt对应文档正文的侧栏说明见 src/ch05-01-defining-structs.md。第 10 章会讲解如何修复这类错误在那之前建议像User示例一样用String等自有类型代替str之类的引用。实战示例用结构体计算矩形面积为了理解何时该用结构体我们编写一个计算矩形面积的小程序从独立变量开始逐步重构到使用结构体对应完整讲解见 src/ch05-02-example-structs.md。第一版两个独立变量用 Cargo 新建名为rectangles的二进制项目在src/main.rs中计算以像素为单位的矩形面积见 listings/ch05-using-structs-to-structure-related-data/listing-05-08/src/main.rsfn 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的函数签名暴露了问题它声称计算“一个矩形”的面积却接收两个参数程序中没有任何信息表明这两个参数彼此关联。把宽高分组在一起会更具可读性、更易维护。第二版重构为元组第 3 章已介绍过一种分组方式——元组见 listings/ch05-using-structs-to-structure-related-data/listing-05-09/src/main.rsfn 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是下标0height是下标1。如果要在屏幕上绘制矩形混淆宽高就会出错这种依赖下标记性的写法很容易引入错误。第三版重构为结构体用结构体为数据添加语义既给整体命名也给各部分命名见 listings/ch05-using-structs-to-structure-related-data/listing-05-10/src/main.rsstruct 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的width和height字段计算面积——宽高彼此关联且有了描述性名字不再依赖0和1两个下标清晰度显著提升。用派生 trait 增加功能打印与调试调试程序时我们希望打印Rectangle实例并查看所有字段的值。直接像之前章节那样用println!宏会失败见 listings/ch05-using-structs-to-structure-related-data/listing-05-11/src/main.rs报错核心信息为error[E0277]: Rectangle doesnt implement std::fmt::Display原因在于println!默认使用{}占位符对应名为Display的格式化 trait其输出面向最终用户。此前见到的原生类型只有一种展示方式所以默认实现了Display但结构体的展示方式存在多种可能要不要逗号要不要大括号要不要展示全部字段Rust 不做猜测因此结构体没有内置Display实现。继续阅读错误会看到很有帮助的提示output.txt中的第 910 行help: the trait Debug is implemented for Rectangle, but formatting is disabled试试看把调用改成println!(rect1 is {rect1:?});。占位符里的:?告诉println!使用名为Debug的输出格式这种格式面向开发者便于调试时查看值。然而仍会报错因为必须显式选择为结构体开启调试信息功能在结构体定义前加外部属性#[derive(Debug)]见 listings/ch05-using-structs-to-structure-related-data/listing-05-12/src/main.rs#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!(rect1 is {rect1:?}); }运行输出$ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized debuginfo] target(s) in 0.31s Running target/debug/rectangles rect1 is Rectangle { width: 30, height: 50 }输出虽不美观但展示了实例所有字段的值调试时非常有用。对更大的结构体可用{:#?}获得更易读的美化输出rect1 is Rectangle { width: 30, height: 50, }对应测试输出记录在 listings/ch05-using-structs-to-structure-related-data/output-only-02-pretty-debug/output.txt。用dbg!宏调试表达式另一种以Debug格式打印值的方式是dbg!宏。与println!接收引用不同dbg!接管表达式的所有权打印宏调用所在文件的路径与行号、表达式的求值结果然后把值返回给调用方。注意dbg!打印到标准错误流stderrprintln!打印到标准输出流stdout。二者的区别将在第 12 章“将错误重定向到标准错误”一节src/ch12-06-writing-to-stderr-instead-of-stdout.md详述。下面的例子同时关注赋给width字段的值和rect1整体见 listings/ch05-using-structs-to-structure-related-data/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] 30 * scale 60 [src/main.rs:14] rect1 Rectangle { width: 60, height: 50, }第一行来自src/main.rs第 10 行调试的表达式30 * scale结果为60整数类型的Debug格式只打印其值第 14 行的dbg!输出rect1的Rectangle值采用漂亮的Debug格式。因为dbg!返回表达式的值width字段拿到了与不加dbg!时相同的值60而对rect1我们不想让dbg!接管所有权所以传入引用。除Debug外Rust 还为derive属性提供了若干 trait 以给自定义类型添加有用行为完整清单见 附录 C可派生 trait。第 10 章会介绍自定义实现与创建 trait 的方法更多其他属性可查阅 Rust Reference 的 “Attributes” 部分。方法Methods把行为绑定到类型上我们的area函数非常特定只计算矩形的面积。把它绑定到Rectangle类型上会更好——它不会对其他类型生效。接下来把area重构为定义在Rectangle上的方法完整讲解见 src/ch05-03-method-syntax.md。用impl块定义方法方法与函数相似都用fn关键字声明、可以有参数和返回值、包含被调用时执行的代码。区别在于方法定义在结构体或枚举、trait 对象分别见第 6 章与第 18 章的上下文内且第一个参数永远是self代表调用该方法的实例。把area变为Rectangle的方法见 listings/ch05-using-structs-to-structure-related-data/listing-05-13/src/main.rs#[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() ); }要点如下用implimplementation实现块开启Rectangle的上下文块内所有内容都与Rectangle类型关联把area移入impl大括号内第一个也是唯一一个参数改为selfmain中改用方法语法调用实例名.方法名()及参数签名中self是self: Self的缩写。impl块内Self是impl后类型的别名。方法首参必须是类型为Self且名为self的参数Rust 允许只写self缩写但前面的仍需保留表示借用Self实例。方法与函数在接收者上的三种形态self不可变借用只读数据本例的选择理由与Rectangle相同不想接管所有权只想读取mut self可变借用需要在方法内修改实例self接管所有权通常用于把self变换成其他东西、并阻止调用方继续使用原实例的场景比较少见。使用方法而非函数的主要理由除方法语法、无需在每个签名里重复self的类型外是组织性一个类型能做的所有事情集中在一个impl块里使用我们代码的人不必在库的各处翻找Rectangle的能力。方法名与字段名相同getter 的雏形方法可以故意取与字段相同的名字。例如给Rectangle定义一个也叫width的方法见 listings/ch05-using-structs-to-structure-related-data/no-listing-06-method-field-interaction/src/main.rsimpl 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); } }width方法在实例的width字段大于0时返回true为0时返回false——字段与方法同名可以各司其职rect1.width带括号是调用方法rect1.width不带括号是访问字段。这类“只返回字段值、不做其他事”的方法称为getterRust 不会像某些语言那样为结构体字段自动生成 getter。getter 的价值在于字段设为私有、方法设为公开即可把对该字段的只读访问作为类型公开 API 的一部分pub关键字详见第 7 章 src/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.md。为什么没有-运算符自动引用与解引用C/C 用.调用对象上的方法用-先解引用指针再调用方法object-something()等价于(*object).something()。Rust 没有-运算符取而代之的是自动引用与解引用automatic referencing and dereferencing——方法调用是 Rust 中少数具备此行为的地方。当你写object.something()时Rust 自动补上、mut或*使object匹配方法的签名。以下两种写法完全等价p1.distance(p2); (p1).distance(p2);第一种显然更简洁。这套机制之所以成立是因为方法有明确的接收者——self的类型。给定接收者与方法名Rust 能确切判断方法是读取self、修改mut self还是消费self。Rust 让方法接收者的借用变成隐式行为是所有权在实际使用中变得顺手的重要原因。带多个参数的方法can_hold再为Rectangle实现第二个方法判断当前实例能否完全容纳另一个Rectangle实例能则返回true否则返回false。先看预期用法见 listings/ch05-using-structs-to-structure-related-data/listing-05-14/src/main.rsfn 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的两个维度都小于rect1而rect3比rect1宽预期输出为Can rect1 hold rect2? true Can rect1 hold rect3? false方法定义在impl Rectangle块内名为can_hold接收另一个Rectangle的不可变借用调用处传的是rect2我们只需读取rect2而无需写入且希望main保留rect2的所有权以便之后继续使用。返回值是布尔值实现比较self与另一个矩形的宽和高见 listings/ch05-using-structs-to-structure-related-data/listing-05-15/src/main.rsimpl Rectangle { fn area(self) - u32 { self.width * self.height } fn can_hold(self, other: Rectangle) - bool { self.width other.width self.height other.height } }方法可以有多个参数写在self之后即可行为与函数参数无异。关联函数Associated Functions不需要实例的构造函数impl块内定义的所有函数统称关联函数associated functions因为它们与impl后的类型相关联。其中不把self作为首参、因而也不属于方法的关联函数不需要实例即可调用——你已经用过类似的东西String::from。这类关联函数常被用作构造函数返回结构体的新实例习惯命名为new——但注意new并非语言内置的特殊名字。例如提供名为square的关联函数用一个尺寸参数同时作为宽和高方便创建正方形矩形见 listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rsimpl Rectangle { fn square(size: u32) - Self { Self { width: size, height: size, } } } fn main() { let sq Rectangle::square(3); }返回类型与函数体里的Self都是impl关键字后类型的别名此处即Rectangle。调用关联函数使用::语法加结构体名let sq Rectangle::square(3);。::语法同时用于关联函数和模块命名空间模块相关内容见第 7 章 src/ch07-02-defining-modules-to-control-scope-and-privacy.md。多个impl块每个结构体允许有多个impl块。例如把area与can_hold拆到两个块中与合并在一个块里完全等价见 listings/ch05-using-structs-to-structure-related-data/listing-05-16/src/main.rsimpl 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块真正有用的场景。小结与延伸阅读结构体让你能创建对领域有意义的自定义类型把相关联的数据片段绑在一起并为每块数据命名让代码清晰可读在impl块中可以定义与类型关联的函数方法则是允许实例具备特定行为的那一类关联函数。结构体并非创建自定义类型的唯一途径——下一站是第 6 章的枚举enum它将为你的工具箱再添一件利器见 src/ch06-00-enums.md。本指南对应的仓库源码脉络章节正文src/ch05-00-structs.md、src/ch05-01-defining-structs.md、src/ch05-02-example-structs.md、src/ch05-03-method-syntax.md全部可编译示例与输出目录 listings/ch05-using-structs-to-structure-related-data/ 下的listing-05-01至listing-05-16以及各no-listing-*与output-only-*子目录相关前置知识所有权与移动src/ch04-01-what-is-ownership.md、元组类型src/ch03-02-data-types.md、可派生 trait 清单src/appendix-03-derivable-traits.md。所有示例均可直接通过cargo run在对应 listing 目录下运行验证如cd listings/ch05-using-structs-to-structure-related-data/listing-05-13 cargo run。赞分享教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载相关推荐Rust 编程语言The Rust Programming Language用结构体 Struct 组织相关联的数据Rust 编程语言The Rust Programming Language用结构体 Struct 组织相关联的数据 结构体struct是 Rust教程文档WTF-Solidity 极简入门Solidity 数组Array与结构体Struct完全指南WTF Solidity 极简入门Solidity 数组Array与结构体Struct完全指南 本讲是 WTF Solidity 极简入门系列的第 6示例工程区块链教程《仓颉语言实战》如何用struct组织数据仓颉结构体定义、构造与使用完整教程《仓颉语言实战》如何用struct组织数据仓颉结构体定义、构造与使用完整教程 《仓颉语言实战》张磊 著清华大学出版社通过 27 个章节帮助零基础读者快速文档/教程前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表