处理Rust对象中的结构体、枚举、特性详细说明这类问题时,先确认目标场景,再按步骤核对配置或玩法细节。
Rust 中的结构体(Struct)与元组(Tuple)都可以将若干个类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫做"字段"。

结构体定义,注意:结尾不需要 ; 符号,而且每个字段定义之后用 , 分隔
struct Site { domain: String, name: String, nation: String, found: u32}Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义:
let runoob = Site { domain: String::from("www.baidu.com"), name: String::from("test"), nation: String::from("China"), found: 2013};这样的好处是不仅使程序更加直观,还不需要按照定义的顺序来输入成员的值,如果正在实例化的结构体有字段名称和现存变量名称一样的,可以简化书写:
let domain = String::from("www.baidu.com");let name = String::from("test");let test = Site { domain, // 等同于 domain : domain, name, // 等同于 name : name, nation: String::from("China"), found: 2026};如果想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法:
let site = Site { domain: String::from("www.baidu.com"), name: String::from("test"), ..test };注意:..test 后面不可以有逗号,这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值。
元组结构体是一种没有字段名、只有类型的结构体,形式上类似元组,它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据
元组结构体对象的使用方式和元组一样,通过 . 和下标来进行访问:
fn main() { struct Color(u8, f64, u8); let black = Color(0, 0.0, 0); println!("black = ({}, {}, {})", black.0, black.1, black.2);}调试中,完整地显示出一个结构体实例是非常有用的。但如果我们手动的书写一个格式会非常的不方便。所以 Rust 提供了一个方便地输出一整个结构体的方法,结构体上导入调试库 #[derive(Debug)]
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:?}", rect1);}之后在 println 和 print 宏中就可以用 {:?} 占位符输出一整个结构体
方法(Method)和函数(Function)类似,只不过它是用来操作结构体实例的。
Rust 语言不是面向对象的,从它所有权机制的创新可以看出这一点。结构体方法的第一个参数必须是 &self,不需声明类型,因为 self 不是一种风格而是关键字。
在调用结构体方法的时候不需要填写 self ,这是出于对使用方便性的考虑。
struct Rectangle { width: u32, height: u32,}impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn wider(&self, rect: &Rectangle) -> bool { self.width > rect.width }}fn main() { let rect1 = Rectangle { width: 30, height: 50 }; let rect2 = Rectangle { width: 40, height: 20 };println!("rect1's area is {}", rect1.area()); println!("{}", rect1.wider(&rect2));}运行结果:false在 Rust 中,self 和 Self 仅差一个字母大小写,但含义完全不同
| self | Self | |
|---|---|---|
| 含义 | 方法首参数的变量名(当前实例本身) | 类型本身的别名 |
| 类比 | 类似 Java 中的 this | 类似 Java 中的"当前类名" |
| 位置 | 出现在方法的第一个参数 | 出现在返回类型、字段类型等位置 |
| 本质 | 一个绑定到实例的变量 | 一个类型 |
Self 只能在 impl 块或 trait 定义内部使用。在普通函数顶层,得写具体类型名
之所以"结构体方法"不叫"结构体函数"是因为"函数"这个名字留给了这种函数:它在 impl 块中却没有 &self 参数。
这种函数不依赖实例,但是使用它需要声明是在哪个 impl 块中的,String::from 函数就是一个"关联函数"。
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}impl Rectangle { fn create(width: u32, height: u32) -> Rectangle { Rectangle { width, height } }}fn main() { let rect = Rectangle::create(30, 50); println!("{:?}", rect);}运行结果:Rectangle { width: 30, height: 50 }结构体 impl 块可以写几次,效果相当于它们内容的拼接
单元结构体(Unit Struct)没有任何字段,它只是一个"类型标记",实例本身不占实际数据空间(编译后大小为 0)。它常用于:只需要一个类型,但不需要存储任何数据的场景。
struct UnitStruct;fn main() { let u = UnitStruct; // 创建实例,什么数据都没有}和其他结构体的对比
struct UnitStruct; // 无字段struct TupleStruct(i32, i32); // 有字段,无字段名struct NormalStruct { x: i32 } // 有字段,有字段名三者的区别就在于有没有数据、数据有没有名字。单元结构体是最简单的一种——纯粹作为一个类型存在,用来实现某个 trait 或者做类型标记,而不携带任何数据。
Rust 枚举的每个变体(variant)可以携带三种形式的数据:无数据、元组式数据、结构体式数据。三种可以在同一个枚举里混用。
enum Message { Quit, // 无数据(类似单元结构体) Move { x: i32, y: i32 }, // 结构体式:字段有名字 Write(String), // 元组式:只有类型,按位置 ChangeColor(i32, i32, i32), // 元组式,多个值}简单使用示例:
fn main() { let msgs = vec![ Message::Quit, Message::Move { x: 10, y: 20 }, Message::Write(String::from("hello")), Message::ChangeColor(255, 0, 0), ]; for msg in msgs { match msg { Message::Quit => { println!("退出"); } Message::Move { x, y } => { println!("移动到 ({}, {})", x, y); // 按字段名解构 } Message::Write(text) => { println!("写入: {}", text);// 按位置解构 } Message::ChangeColor(r, g, b) => { println!("颜色: ({}, {}, {})", r, g, b); } } }}枚举变体也能有方法
impl Message { fn call(&self) { match self { Message::Write(text) => println!("{}", text), _ => println!("其他消息"), } }}let m = Message::Write(String::from("hi"));m.call(); // hiRust 通过 match 语句来实现分支结构。先认识一下如何用 match 处理枚举类:
fn main() { enum Book { Papery {index: u32}, Electronic {url: String}, } let book = Book::Papery{index: 1001}; let ebook = Book::Electronic{url: String::from("url...")}; match book { Book::Papery { index } => { println!("Papery book {}", index); }, Book::Electronic { url } => { println!("E-book {}", url); } }}match 块也可以当作函数表达式来对待,它也是可以有返回值的,但是所有返回值表达式的类型必须一样
match 枚举类实例 { 分类1 => 返回值表达式, 分类2 => 返回值表达式, ...}对于枚举变体中元组数据,在 match 块中需要临时指定一个名字,对于普通结构体变体,可以通过 字段名:变量名 重命名
enum Book { Papery(u32), Electronic {url: String},}let book = Book::Papery(1001);match book { Book::Papery(i) => { //临时名字 println!("{}", i); }, Book::Electronic { url } => { //使用原来名字 println!("{}", url); } // 或者显式重命名:字段名:变量名 Book::Electronic { url: link } => { println!("{}", link); // 变量名是 link }, // 甚至可以忽略值 Book::Electronic { url: _ } => { println!("has url but ignored"); },}对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事,例外情况用下划线 _ 表示:
fn main() { let t = "abc"; match t { "abc" => println!("Yes"), _ => {}, }}ref 用在 match 模式里——按引用绑定(防止所有权被 move 走)
let opt = Some(String::from("hello"));match opt { Some(ref s) => println!("{}", s), // ref:s 是 &String,内容的所有权不动 None => {}}println!("{:?}", opt); // opt 还能用——如果没写 ref,String 早被 move 进分支了ref 的场景:match会按值匹配结构体/枚举里的内容,默认把字段move进绑定变量;加ref改成"绑定一个指向字段的引用",原值所有权保留。对应的还有ref mut(绑定 &mut`)。
但现代 Rust 里 ref 基本绝迹了,因为2018 版引入了 match ergonomics(匹配人体工学):当 match 一个引用时,绑定模式会自动降级成引用,不用手写 ref
let opt = Some(String::from("hello"));match &opt { // match 的是引用 Some(s) => println!("{}", s), // s 自动就是 &String,不用 ref None => {}}println!("{:?}", opt); // ✅ 一样不 moveif let 语法格式如下:
if let 模式(枚举变体) = 源变量 { 语句块}注意:此处是一个等号而非俩个等号,因为做的是模式绑定,不是 相等比较,if let 本质上是 let 的延伸,尝试做解构绑定(把一个复合值拆开,把里面的各个部分分别绑定到新的变量上。可以理解为拆箱+命名一步完成)
使用示例
let i = 0;match i { 0 => println!("zero"), _ => {},}放入主函数运行结果:zero// 现在用 if let 语法缩短这段代码:let i = 0;if let 0 = i { println!("zero");}可以在之后添加一个 else 块来处理例外情况。
if let 语法可以认为是只区分两种情况的 match 语句的"语法糖"。
对于枚举类依然适用:
fn main() { enum Book { Papery(u32), Electronic(String) } let book = Book::Electronic(String::from("url")); if let Book::Papery(index) = book { println!("Papery {}", index); } else { println!("Not papery book"); }}Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持 null 引用的空白。Rust 在语言层面彻底不允许空值 null 的存在,但无奈null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类:
enum Option<T> { Some(T), None,}如果要定义一个可以为空值的类,可以这样:let opt = Option::Some("Hello");
如果想针对 opt 执行某些操作,必须先判断它是否是 Option::None:
fn main() { let opt = Option::Some("Hello"); match opt { Option::Some(something) => { println!("{}", something); }, Option::None => { println!("opt is nothing"); } }}初始值为空的 Option 必须明确类型:
fn main() { let opt: Option<&str> = Option::None; match opt { Option::Some(something) => { println!("{}", something); }, Option::None => { println!("opt is nothing"); } }}运行结果:opt is nothing这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略 Option:: 直接写 None 或者 Some()。Option 是一种特殊的枚举类,它可以含值分支选择:
fn main() { let t = Some(64); match t { Some(64) => println!("Yes"), _ => println!("No"), }}特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法
特性在 Rust 中用 trait 表示:Descriptive 规定了实现者必需有 describe(&self) -> String 方法。
trait Descriptive { fn describe(&self) -> String;}用特性实现一个结构体,格式是:impl <特性名> for <所实现的类型名>
简单例子:
struct Person { name: String, age: u8}impl Descriptive for Person { fn describe(&self) -> String { format!("{} {}", self.name, self.age) }}Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个
这是特性与接口的不同点:接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法:
trait Descriptive { fn describe(&self) -> String { String::from("[Object]") }}struct Person { name: String, age: u8}impl Descriptive for Person { fn describe(&self) -> String { format!("{} {}", self.name, self.age) }}fn main() { let cali = Person { name: String::from("Cali"), age: 24 }; println!("{}", cali.describe());}运行结果:Cali 24很多情况下我们需要传递一个函数做参数,例如回调函数、设置按钮事件等。在 Rust 中可以通过传递特性参数来实现:
fn output(object: impl Descriptive) { println!("{}", object.describe());}任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没必要了解传入对象有没有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内也无法使用其他的属性与方法。
特性参数还可以用这种等效语法实现:
fn output<T: Descriptive>(object: T) { println!("{}", object.describe());}如果有多个参数类型均是特性:
fn output_two<T: Descriptive>(arg1: T, arg2: T) { println!("{}", arg1.describe()); println!("{}", arg2.describe());}特性作类型表示时如果涉及多个特性,可以用 + 符号表示,例如:
fn notify(item: impl Summary + Display)fn notify<T: Summary + Display>(item: T)
注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。
复杂的实现关系可以使用 where 关键字简化,例如:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)可以简化成:fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone,U: Clone + Debug
特性做返回值格式如下:
fn person() -> impl Descriptive { Person { name: String::from("Cali"), age: 24 }}但是有一点,特性做返回值只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。比如结构体 A 与结构体 B 都实现了特性 Trait,下面这个函数就是错误的:
fn some_function(bool bl) -> impl Descriptive { if bl { return A {}; } else { return B {}; }}dyn全称 dynamic dispatch(动态分发),dyn Trait ≈ Java 里的"接口类型引用"——运行期多态。
先看 Rust 的两种多态,dyn 是其中一种
泛型 <T: Trait> | dyn Trait | |
|---|---|---|
| 类比 Java | 更像 C++ 模板(Java 没有对应物) | 就是 Java 的接口引用 |
| 分发时机 | 编译期 | 运行期(vtable 查找) |
| 类型 | 编译期已知,单态化 | 擦除成统一的 trait 对象 |
| 性能 | 零成本,可内联 | 一次间接跳转 |
| 异构集合 | ❌ Vec 里只能放一种 T | ✅ Vec<Box<dyn Draw>> 混放圆和方 |
| 方法可否泛型 | 可以 | ❌(object safety 限制) |
// ① 静态分发(泛型):编译期为每个具体类型生成专门代码fn draw_static<T: Draw>(shape: T) { ... }// 编译后相当于生成了 draw_circle、draw_square 两个函数// ② 动态分发(dyn):类型被擦除,运行期通过 vtable 找方法fn draw_dynamic(shape: &dyn Draw) { ... }// 只有一个函数,运行时才查"这个对象到底是哪个类型"为什么dyn 必须装在指针里
不同实现类型大小不一样(Circle 4 字节,Square 8 字节),擦除类型后编译器不知道它占多少空间——dyn Trait 是个不定长类型(unsized),不能直接放栈上。其中
&就是指针。&dyn Draw里的&是"引用"(reference),而Rust的引用本质上就是指针,带编译期安全检查的那种
所以永远要包一层指针:
Box<dyn Error> // 堆上,独占&dyn Draw // 借用 = 指针 Arc<dyn Future> // 共享,跨线程
胖指针 vs 普通(瘦)指针:
64 位机器上,一个内存地址是 8 字节。普通引用就一个地址,但 &dyn Draw 里类型被擦除了,运行期得知道去哪个类型的方法表查,所以多带一个 vtable 地址,变成 16 字节——这就是"胖"的由来:
let d: &dyn Draw = &c;// d 占 16 字节:[ 0x1000 | 0x7FF6AB00 ]// │ └──→ vtable(Circle 实现 Draw 的方法表)// └────────────→ Circle 的数据本体
vtable:编译器为某个具体类型 × 某个 trait的组合生成一张静态表,内容是:
vtable(Circle as Draw):┌─────────────────────────────────┐│ drop_in_place::<Circle> │ ← 析构函数指针(Box<dyn> 丢弃时要调真正的析构)│ size_of::<Circle> = 4 │ ← 大小(Box 释放堆内存需要知道布局)│ align_of::<Circle> = 4 │ ← 对齐(同上)│ <Circle as Draw>::draw │ ← 方法的函数指针└─────────────────────────────────┘
trait Draw { fn draw(&self);}struct Circle { r: i32,}impl Draw for Circle { fn draw(&self) { println!("画一个半径 {} 的圆", self.r); }}struct Square { side: i32,}impl Draw for Square { fn draw(&self) { println!("画一个边长 {} 的正方形", self.side); }}// 瘦指针参数:只认 Circle,静态分发fn draw_thin(shape: &Circle) { shape.draw();}// 胖指针参数:任何实现了 Draw 的类型都行,动态分发fn draw_fat(shape: &dyn Draw) { shape.draw();}#[test]fn test_thin_vs_fat() { let c = Circle { r: 5 }; let s = Square { side: 3 }; // ① 瘦指针:8 字节 = 只有数据地址 let thin: &Circle = &c; assert_eq!(std::mem::size_of::<&Circle>(), 8); draw_thin(thin); // 编译期就定死调 <Circle as Draw>::draw // ② 胖指针:16 字节 = 数据地址 + vtable 地址 let fat: &dyn Draw = &c; // &Circle 自动"增肥"成 &dyn Draw assert_eq!(std::mem::size_of::<&dyn Draw>(), 16); draw_fat(fat); // 运行期查 vtable 找 draw // ③ 胖指针的看家本领:异构集合 let shapes: Vec<&dyn Draw> = vec![&c, &s]; // 圆和方混装进一个 Vec for shape in shapes { shape.draw(); // 每个元素运行期各查各的 vtable }}let fat: &dyn Draw = &c; 这行发生了什么
编译器执行了一次"增肥"(
unsized coercion):把&Circle的瘦指针,拼上Circle as Draw的vtable地址,组装成 16 字节的胖指针。&s转&dyn Draw时拼的则是Square的vtable——同一个&dyn Draw类型,里面装的表不一样,运行时行为自然不同。
第 ③ 处的 Vec 是 dyn 存在的核心理由
由于
Vec要求元素类型统一,瘦指针&Circle和&Square是两个不同类型,混装不了;但擦除成&dyn Draw后统一成同一个类型了,随便混