Rust 零大小类型(ZST)

embedded/2025/1/17 8:09:09/

在 Rust 中,零大小类型(Zero-Sized Type,简称 ZST) 是指在内存中不占用任何存储空间的类型。这些类型的大小为 0 字节,编译器会对它们进行优化,避免为它们分配实际的存储空间。ZST 是 Rust 类型系统中一个非常重要的概念,通常用于标志性的用途(marker types)或类型级别的计算。

1. 零大小类型的定义

零大小类型的特点
  1. 没有任何数据字段:结构体、枚举或类型本身不包含任何数据。
  2. 大小为 0 字节:它们在内存中不占用实际的空间。
  3. 编译器优化:ZST 可以在编译时被优化掉,多个 ZST 的实例在内存中不会重复分配。
示例:ZST 的定义
空结构体
rust">struct Empty;fn main() {let e1 = Empty;let e2 = Empty;println!("Size of Empty: {}", std::mem::size_of::<Empty>());println!("e1 address: {:p}\ne2 address: {:p}", &e1, &e2);println!("Are e1 and e2 the same? {}", std::ptr::eq(&e1, &e2)); 
}

输出:

Size of Empty: 0
e1 address: 0x7ffecbf97d3e
e2 address: 0x7ffecbf97d3f
Are e1 and e2 the same? false
空枚举
rust">enum EmptyEnum {}fn main() {println!("Size of EmptyEnum: {}", std::mem::size_of::<EmptyEnum>()); // 输出 0
}
单元类型(()
rust">fn main() {let unit = ();println!("Size of unit type: {}", std::mem::size_of::<()>()); // 输出 0
}

2. 为什么需要零大小类型?

ZST 的存在是因为 Rust 的类型系统要求所有类型都有一个明确的定义和行为,即使某些类型在运行时根本不需要实际的数据存储。

以下是 ZST 的一些常见用途:

(1) 标志类型(Marker Types)

ZST 可以作为标志性类型,用来描述某些行为或特性。例如,PhantomData 是一个 ZST,常用于表示类型中的占位符,尤其在泛型编程中。

rust">use std::marker::PhantomData;struct MyType<T> {_marker: PhantomData<T>, // 不占用内存,只用于标记类型 T
}fn main() {let instance: MyType<u32> = MyType { _marker: PhantomData };println!("Size of MyType<u32>: {}", std::mem::size_of::<MyType<u32>>()); // 输出 0
}
(2) 单元类型(()

单元类型是一个 ZST,用于表示无返回值的函数或某些操作的结果。例如:

rust">fn do_nothing() {}fn main() {let result = do_nothing(); // `result` 的类型是 `()`println!("Size of unit type: {}", std::mem::size_of::<()>()); // 输出 0
}
  • 单元类型通常作为函数的默认返回类型。
  • () 表示“什么都没有”,但在类型系统中需要明确地表示这种空值。
(3) 用作占位符(Placeholder)

ZST 可以用来表示某些需要类型约束的情况下的占位符,而不需要实际的数据。例如,定义一个 ZST 来实现某些特征,而无需实际存储数据:

rust">struct NoData;impl NoData {fn new() -> Self {NoData}
}fn main() {let x = NoData::new();println!("Size of NoData: {}", std::mem::size_of::<NoData>()); // 输出 0
}
(4) 优化内存占用

Rust 编译器会对 ZST 进行优化,比如在容器中存储 ZST 时,编译器会避免为它分配额外的空间。

rust">use std::alloc::{Layout, System};
struct Empty;fn main() {// 验证 Vec<Empty> 的堆分配let vec_empty: Vec<Empty> = Vec::with_capacity(10);let layout_empty = Layout::array::<Empty>(vec_empty.capacity()).unwrap();println!("Vec<Empty> capacity: {}", vec_empty.capacity());println!("Vec<Empty> heap allocation size: {}", layout_empty.size()); // 输出 0 字节// 验证 Vec<i32> 的堆分配let vec_i32: Vec<i32> = Vec::with_capacity(10);let layout_i32 = Layout::array::<i32>(vec_i32.capacity()).unwrap();println!("Vec<i32> capacity: {}", vec_i32.capacity());println!("Vec<i32> heap allocation size: {}", layout_i32.size()); // 输出 40 字节 (10 * 4)
}

输出:

Vec<Empty> capacity: 18446744073709551615    # 0xFFFFFFFFFFFFFFFF
Vec<Empty> heap allocation size: 0
Vec<i32> capacity: 10
Vec<i32> heap allocation size: 40
  • 这里的 Vec<Empty> 存储了 3 个 ZST 实例,但由于 ZST 的大小为 0,编译器优化掉了实际的存储。
  • Vec 本身仍然需要存储元信息(如容量、长度等),因此它占用固定的内存(通常为 24 字节)。

3. 如何检查类型的大小?

你可以使用 std::mem::size_of 函数来检查任何类型的大小:

rust">use std::mem;struct Empty;
struct Data {x: i32,y: i32,
}fn main() {println!("Size of Empty: {}", mem::size_of::<Empty>()); // 输出 0println!("Size of Data: {}", mem::size_of::<Data>());   // 输出 8println!("Size of (): {}", mem::size_of::<()>());       // 输出 0
}

4. 注意事项

虽然 ZST 的大小为 0,但在以下情况下需要注意:

(1) 指针的行为

即使是 ZST,引用它们的指针仍然占用内存(通常是一个机器字大小,比如 8 字节)。

rust">struct Empty;fn main() {let e1 = Empty;let e2 = Empty;// 即使 ZST 的实例没有大小,但它们的引用是有效的println!("Size of &Empty: {}", std::mem::size_of::<&Empty>()); // 输出 8(指针大小)
}
(2) 不能创建空枚举的实例

如果一个枚举没有任何变体,它的大小仍然是 0,但你无法创建它的实例:

rust">#[derive(Debug)]
enum EmptyEnum {}fn main() {let x: EmptyEnum; // 可以声明// let x: EmptyEnum = EmptyEnum; // 无法创建实例println!("Size of EmptyEnum: {}", std::mem::size_of::<EmptyEnum>()); // 输出 0
}

5. 总结

  • 零大小类型(ZST) 是一种占用 0 字节内存 的类型,常用于标志、单元类型、占位符等场景。
  • 常见 ZST
    • 空结构体(struct Empty;
    • 空枚举(enum EmptyEnum {}
    • 单元类型(()
    • 标志性类型(如 PhantomData
  • 用途
    • 节省内存
    • 编译时标志性用途
    • 泛型占位符或类型约束
  • 重要特性
    • 多个实例在内存中没有区别(指针地址可能相同)。
    • 虽然值本身没有大小,但它们的引用(指针)仍然需要占用内存。

ZST 是 Rust 类型系统的一个独特设计,提供了高效和灵活的方式来表达类型信息,同时避免了多余的运行时开销。

例子

例 1

rust">use std::ops::Deref;struct Parent;impl Parent {fn say_hello(&self) {println!("Hello from Parent");}
}struct Child;impl Deref for Child {type Target = Parent;fn deref(&self) -> &Self::Target {&Parent}
}fn main() {let child = Child;child.say_hello(); 
}

例 2

rust">use std::ops::Deref;struct Parent{pub data: String,
}impl Parent {fn say_hello(&self) {println!("Hello from Parent: {}", self.data);}fn update_data(&mut self, data: String) {self.data = data;}
}struct Child {pub inner: Parent,
}impl Deref for Child {type Target = Parent;fn deref(&self) -> &Self::Target {&Parent { data: "Initial Data".to_string() }}
}fn main() {let child = Child;child.say_hello(); 
}

第一个可以编译,第二个报错:

error[E0515]: cannot return reference to temporary value--> src/main.rs:24:9|
24 |         &Parent { data: "Initial Data".to_string() }|         ^-------------------------------------------|         |||         |temporary value created here|         returns a reference to data owned by the current function

分析

这里的核心区别在于值的生命周期以及它们是如何分配和存储的。在 Rust 中,静态值static lifetime)和临时值(temporary value)的区别,决定了它们的生命周期及能否被返回引用。

例 1 返回的 &Parent 是指向一个静态生命周期的值,因为 Parent 是一个全局的静态变量。换句话说,它是一个固定的内存地址,在程序整个运行期间始终有效。静态值满足 Rust 的生命周期规则,编译器能够确保这个引用是安全的。
例 2 试图返回一个引用,指向一个临时值Parent { data: ... }),而这个临时值在函数结束后会被释放。Rust 的生命周期检查器会阻止这种行为,因为返回的引用会变得无效,可能导致悬垂引用(Dangling Reference)。

为什么 &Parent 是静态值,而 &Parent { data: "Initial Data".to_string() } 不是?

1. Parent 是一个零大小类型(ZST)

在例 1 中,Parent 只是一个没有任何字段的结构体:

rust">struct Parent;

因为它没有字段,所以它占用的内存大小为 0 字节(零大小类型,ZST)。编译器可以优化这个类型的使用,将其视为一个全局的静态常量。因此,&Parent 是一个指向静态内存的引用,具有 'static 生命周期。也就是说,&Parent 的地址是固定的,它可以安全地被返回。

2. Parent { data: "Initial Data".to_string() } 是一个动态分配的值
rust">struct Parent { pub data: String, 
}

String 是一个动态分配的类型,存储在堆上。当你写:

rust">Parent { data: "Initial Data".to_string() }

Rust 会在堆上分配一段内存来存储字符串 "Initial Data",并在栈上存储 Parent 结构体实例,里面包含堆分配的 String 的元信息(如指针、长度、容量)。这是一个临时值,它的生命周期只持续到当前作用域(deref 函数体)结束。一旦 deref 返回,这个临时值会被释放,从而导致潜在的悬垂引用。

如何修复这个问题?

如果你希望返回一个有效的引用,可以通过以下几种方法:

方法 1:使用 static 定义一个全局静态值

Parent { data: ... } 定义为一个静态变量,并返回对它的引用:

rust">static INSTANCE: Parent = Parent {data: "Initial Data".to_string(), 
};  fn deref(&self) -> &Self::Target {&INSTANCE 
}
  • 优点:静态值的生命周期是 'static,可以安全返回引用。
  • 缺点:静态值是固定的,无法动态变化。
方法 2:将值存储在结构体中

Parent 的实例存储在 Child 的字段中,返回其引用:

rust">struct Child {inner: Parent,
}impl Deref for Child {type Target = Parent;fn deref(&self) -> &Self::Target {&self.inner}
}
  • 优点:引用的生命周期和 Child 的实例绑定,符合生命周期规则。
  • 缺点:每个 Child 实例需要持有一个 Parent 实例。
方法 3:使用智能指针(BoxRc

Parent 存储在堆上,并通过智能指针管理它的生命周期:

rust">struct Child {inner: Box<Parent>,
}impl Deref for Child {type Target = Parent;fn deref(&self) -> &Self::Target {&self.inner}
}
  • 优点:灵活,支持动态分配。
  • 缺点:引入了一些额外的运行时开销。

总结

  • 为什么 &Parent 是静态值
    • 因为 Parent 是一个零大小类型(ZST),它可以被优化为静态全局常量,生命周期是 'static
  • 为什么 &Parent { data: ... } 不是静态值
    • 因为 Parent { data: ... } 是一个临时值,它的生命周期只存在于当前作用域中,无法返回指向它的引用。
  • 如何解决
    • 使用静态值(static)、将值存储在结构体中,或者使用智能指针管理其生命周期。

Rust 的生命周期规则严格保证了引用的安全性,这也是它在内存安全上的核心优势!


http://www.ppmy.cn/embedded/154600.html

相关文章

【docker下载kaggle国外镜像超时】kaggle比赛中时遇到的问题

Docker拉取镜像时的痛点 当Docker在拉取镜像时遇到拉取超时问题&#xff0c;往往会让用户深感困扰。造成这种现象的原因可能多种多样&#xff0c;其中网络问题是最常见的原因之一。由于Docker的镜像仓库往往部署在远程服务器上&#xff0c;因此当用户网络环境不佳时&#xff0…

MySQL(高级特性篇) 07 章——InnoDB数据存储结构

一、数据库的存储结构&#xff1a;页 索引结构给我们提供了高效的索引方式&#xff0c;不过索引信息以及数据记录都是保存在文件上的&#xff0c;确切地说是存储在页结构中。另一方面&#xff0c;索引是在存储引擎中实现的&#xff0c;MySQL服务器上的存储引擎负责对表中数据的…

milvus过滤功能

数据格式: [{"id": 0, "vector": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592], "color": "pink_8682", "likes": 165},{"id": 1, "vecto…

centos7设置软链接

在CentOS中设置软链接&#xff08;也称为符号链接&#xff09;可以使用ln命令&#xff0c;并使用-s选项来指定软链接。软链接类似于Windows系统中的快捷方式。 以下是创建软链接的基本命令格式&#xff1a; ln -s [目标文件或目录] [软链接名]ln -s /home/user/original_fold…

CSS 圆形头像和破图时显示默认图片

一、需求 1、css实现圆形头像 2、破图是显示默认图片 二、实现 <img :src"photoSrc" class"circle-avatar" :width"size" :height"size" error"handleImageError" //破图时使用的方法 > <style> .circl…

Kivy App开发之UX控件Spinner选择框

Spinner也是一个下拉列表,在选择框中快速地从一组值中选择一个值,默认状态下,Spinner会显示当前text的属性值,点击时会显示一个下拉菜单,从其中选择一个新的值。 常用属性如下 属性说明values下拉列表的值,默认空列表[]is_open是否展开,默认falsesync_height是否更改下…

《C++11》静态断言(Static Assert)的使用与优势

C11引入了许多新特性&#xff0c;其中之一就是静态断言&#xff08;Static Assert&#xff09;。这是一种在编译时期进行断言的机制&#xff0c;它可以帮助我们在编译阶段就发现错误&#xff0c;而不是等到运行时才发现。这样可以大大提高代码的质量和稳定性。本文将详细介绍静…

Spring boot框架下的RocketMQ消息中间件

1. RocketMQ 基础概念 1.1 核心概念 以下是 RocketMQ 核心概念在 Spring Boot 的 Java 后端代码中的实际使用方式&#xff1a; Producer&#xff08;生产者&#xff09; 定义&#xff1a;Producer 是负责发送消息到 RocketMQ 的组件。它可以将消息发送到指定的 Topic。 实…