商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > Rust对象中的结构体、枚举、特性详解

Rust对象中的结构体、枚举、特性详解

  发布于2026-08-05 阅读(0)

扫一扫,手机访问

1 结构体

先从结构体说起。Rust 把数据组织成整体有两种方式:元组和结构体。元组灵活但没名字,结构体给每个字段起了名字,用起来更直观。结构体在代码里很常见,用来定义常用的数据结构,每个成员叫“字段”。

1.1 简介

1.1.1 定义

Rust 的结构体与元组都可以把一堆类型不同的数据捆绑成整体,但结构体给每个成员起了名字,访问的时候不用记下标。元组适合临时传递多个值,结构体适合长期使用的数据结构。结构体的每个成员叫作“字段”。

Rust对象中的结构体、枚举、特性详解

定义结构体时,注意结尾不需要 ;,每个字段后面用逗号分隔:

struct Site {
    domain: String,
    name: String,
    nation: String,
    found: u32
}

1.1.2 结构体实例

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 后面不能加逗号,而且至少得重新设定一个字段的值,不能完全复制。

1.1.3 元组结构体

如果觉得给每个字段起名字太麻烦,元组结构体是个不错的选择——它没有字段名,只有类型,形式上像元组。适合处理那些需要定义类型但又不想太复杂的简单数据。访问方式也和元组一样,通过 . 加下标:

fn main() {
    struct Color(u8, f64, u8);
    let black = Color(0, 0.0, 0);
    println!("black = ({}, {}, {})", black.0, black.1, black.2);
}

1.2 结构体所有权

1.2.1 输出结构体

调试的时候,能一眼看到整个结构体实例的内容太方便了。手动写格式化代码又烦又容易错,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! 宏中就可以用 {:?} 占位符输出整个结构体。

1.2.2 结构体方法

方法(Method)和函数类似,但它是用来操作结构体实例的。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 中,selfSelf 仅差一个字母大小写,但含义完全不同:

selfSelf
含义方法首参数的变量名(当前实例本身)类型本身的别名
类比类似 Java 中的 this类似 Java 中的"当前类名"
位置出现在方法的第一个参数出现在返回类型、字段类型等位置
本质一个绑定到实例的变量一个类型

Self 只能在 impl 块或 trait 定义内部使用。在普通函数顶层,得写具体类型名。

1.2.3 结构体关联函数

没有 &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 块可以写多次,效果相当于它们内容的拼接。

1.2.4 单元结构体

单元结构体(Unit Struct)没有任何字段,它只是一个“类型标记”,实例本身不占实际数据空间(编译后大小为 0)。常用于:只需要一个类型,但不需要存储任何数据的场景。

struct UnitStruct;
fn main() {
    let u = UnitStruct;  // 创建实例,什么数据都没有
}

和其他结构体的对比:

struct UnitStruct;                    // 无字段
struct TupleStruct(i32, i32);         // 有字段,无字段名
struct NormalStruct { x: i32 }		  // 有字段,有字段名

三者的区别就在于有没有数据、数据有没有名字。单元结构体是最简单的一种——纯粹作为一个类型存在,用来实现某个 trait 或者做类型标记,而不携带任何数据。

2 枚举类

2.1 简介

Rust 的枚举比很多语言的 enum 强大得多——每个变体都能带数据,而且可以带不同类型的数据,甚至三种形式混着用:无数据、元组式数据、结构体式数据。

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();  // hi

2.2 match 语法

2.2.1 简介

Rust 通过 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"),
        _ => {},
    }
}

2.2.2 ref 关键字

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); // ✅ 一样不 move

2.3 if let 语法

if 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");
    }
}

2.4 Option 枚举类

Option 是 Rust 标准库中的枚举类,用于填补 Rust 不支持 null 引用的空白。Rust 在语言层面彻底不允许空值,但无奈 null 能高效解决少量问题,所以引入了 Option

enum Option {
    Some(T),
    None,
}

如果要定义一个可以为空值的变量,可以这样:let opt = Option::Some("Hello");。如果想针对 opt 执行操作,必须先判断它是否是 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 是编译器默认引入的,使用时可以省略 Option::,直接写 NoneSome()Option 是一种特殊的枚举类,可以含值分支选择:

fn main() {
        let t = Some(64);
        match t {
                Some(64) => println!("Yes"),
                _ => println!("No"),
        }
}

3 特性

3.1 简介

特性(trait)有点像 Java 的接口,但更灵活。它们都是一种行为规范,可以标识哪些类有哪些方法。特性在 Rust 中用 trait 表示:

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 块只能实现一个。

3.2 默认特性

这是特性与接口的不同点:接口只能规范方法,不能定义方法;但特性可以定义方法作为默认方法。因为是“默认”,所以对象既可以重新定义,也可以不重新定义直接使用默认方法:

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

3.3 特性做参数

3.3.1 作为入参

很多情况下需要传递一个函数做参数,比如回调函数、设置按钮事件等。在 Rust 中可以通过传递特性参数来实现:

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}

任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,函数不需要了解传入对象有没有其他属性或方法,只需要知道它一定有 Descriptive 特性规范的方法。当然,此函数内也无法使用其他属性与方法。

特性参数还有等效语法:

fn output(object: T) {
    println!("{}", object.describe());
}

如果有多个参数类型均是同一特性:

fn output_two(arg1: T, arg2: T) {
    println!("{}", arg1.describe());
    println!("{}", arg2.describe());
}

特性作类型表示时如果涉及多个特性,可以用 + 符号:

fn notify(item: impl Summary + Display)
fn notify(item: T)

注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。

复杂的实现关系可以使用 where 关键字简化,例如:

fn some_function(t: T, u: U)
可以简化成:

fn some_function(t: T, u: U) -> i32 where 
	T: Display + Clone,
	U: Clone + Debug

3.3.2 作为出参

特性做返回值格式如下:

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

3.4 dyn 关键字

3.4.1 简介

dyn 全称 dynamic dispatch(动态分发),dyn Trait 相当于 Java 里的“接口类型引用”——运行期多态。先看 Rust 的两种多态,dyn 是其中一种:

泛型 dyn Trait
类比 Java更像 C++ 模板(Java 没有对应物)就是 Java 的接口引用
分发时机编译期运行期(vtable 查找)
类型编译期已知,单态化擦除成统一的 trait 对象
性能零成本,可内联一次间接跳转
异构集合❌ Vec 里只能放一种 TVec> 混放圆和方
方法可否泛型可以❌(object safety 限制)
// ① 静态分发(泛型):编译期为每个具体类型生成专门代码
fn draw_static(shape: T) { ... }
// 编译后相当于生成了 draw_circle、draw_square 两个函数
// ② 动态分发(dyn):类型被擦除,运行期通过 vtable 找方法
fn draw_dynamic(shape: &dyn Draw) { ... }
// 只有一个函数,运行时才查"这个对象到底是哪个类型"

为什么 dyn 必须装在指针里?因为不同实现类型大小不一样(Circle 4 字节,Square 8 字节),擦除类型后编译器不知道它占多少空间——dyn Trait 是个不定长类型(unsized),不能直接放栈上。其中 & 就是引用,Rust 的引用本质上就是指针,带编译期安全检查的那种。

所以永远要包一层指针:

Box      // 堆上,独占
&dyn Draw           // 借用 = 指针 
Arc     // 共享,跨线程

3.4.2 胖指针和vtable

普通引用是 8 字节(64 位机器),但 &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::         │ ← 析构函数指针(Box 丢弃时要调真正的析构)
│ size_of::   = 4         │ ← 大小(Box 释放堆内存需要知道布局)
│ align_of::  = 4         │ ← 对齐(同上)
│ ::draw          │ ← 方法的函数指针
└─────────────────────────────────┘

3.4.3 实例分析

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); // 编译期就定死调 ::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 Drawvtable 地址,组装成 16 字节的胖指针。&s&dyn Draw 时拼的则是 Squarevtable——同一个 &dyn Draw 类型,里面装的表不一样,运行时行为自然不同。

第 ③ 处的 Vecdyn 存在的核心理由:由于 Vec 要求元素类型统一,瘦指针 &Circle&Square 是两个不同类型,混装不了;但擦除成 &dyn Draw 后统一成同一个类型了,随便混。

本文转载于:https://www.jb51.net/program/368595oa5.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注