Rust 零大小类型(ZST)

server/2025/1/17 21:11:37/

在 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/server/159182.html

相关文章

专题 - STM32

基础 基础知识 STM所有产品线&#xff08;列举型号&#xff09;&#xff1a; STM产品的3内核架构&#xff08;列举ARM芯片架构&#xff09;&#xff1a; STM32的3开发方式&#xff1a; STM32的5开发工具和套件&#xff1a; 若要在电脑上直接硬件级调试STM32设备&#xff0c;则…

ospf收敛特性及其他的小特性

1. 收敛特性 快速收敛&#xff1a;   只第一次计算时计算全部节点Full SPF   增量最短路径优先算法I-SPF&#xff08;Incremental&#xff09;    只对受影响的节点进行路由计算   全部路由计算PRC    只对发生变化的路由进行重新计算;    根据I-SPF 算出来的SPT …

如何解决Webview和H5缓存问题,确保每次加载最新版本的资源

WebView 用于加载 H5 页面是常见的做法&#xff0c;它能够加载远程的 HTML、CSS、JavaScript 资源&#xff0c;并且让 Web 应用嵌入到原生 App 中。然而&#xff0c;WebView 的缓存机制有时会导致用户看到的是旧版本的页面或资源&#xff0c;尤其是在 H5 发版后&#xff0c;iOS…

【数模学习笔记】插值算法和拟合算法

声明&#xff1a;以下笔记中的图片以及内容 均整理自“数学建模学习交流”清风老师的课程资料&#xff0c;仅用作学习交流使用 文章目录 插值算法定义三个类型插值举例插值多项式分段插值三角插值 一般插值多项式原理拉格朗日插值法龙格现象分段线性插值 牛顿插值法 Hermite埃尔…

剑指Offer|LCR 031. LRU 缓存

LCR 031. LRU 缓存 运用所掌握的数据结构&#xff0c;设计和实现一个 LRU (Least Recently Used&#xff0c;最近最少使用) 缓存机制 。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存…

如何防止服务器被入侵

要防止服务器被入侵&#xff0c;首先需要了解黑客入侵服务器的几条途径&#xff0c;经护卫神安全团队整理&#xff0c;黑客入侵大概有四条途径&#xff1a; 1、利用网站漏洞入侵 2、利用系统漏洞入侵 3、利用软件漏洞入侵 4、利用远程桌面入侵 我们需要对这些途径都做好防…

浅谈云计算18 | OpenStack架构概述

OpenStack架构概述 一、OpenStack核心组件探究1.1 计算组件Nova1.2 镜像组件Glance1.3 身份认证组件Keystone1.4 网络组件Neutron1.5 块存储组件Cinder1.6 对象存储组件Swift1.7 控制面板组件Horizon1.8 计量组件Ceilometer1.9 编排组件Heat 二、OpenStack组件逻辑关系揭秘2.1 …

jenkins 入门到精通

忘记密码 1.以root用户进入jenkins容器中 docker exec -it --user root [jenkens] bash 2.找配置文件 config.xml find / -name config.xml 3.编辑 config.xml 文件 sed s/<useSecurity>true<\/useSecurity>/<useSecurity>false<\/useSecurity>/g…