当前位置: 首页 > article >正文

Rust 零大小类型(ZST)

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

1. 零大小类型的定义

零大小类型的特点
  1. 没有任何数据字段:结构体、枚举或类型本身不包含任何数据。
  2. 大小为 0 字节:它们在内存中不占用实际的空间。
  3. 编译器优化:ZST 可以在编译时被优化掉,多个 ZST 的实例在内存中不会重复分配。
示例:ZST 的定义
空结构体
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
空枚举
enum EmptyEnum {}

fn main() {
    println!("Size of EmptyEnum: {}", std::mem::size_of::<EmptyEnum>()); // 输出 0
}
单元类型(()
fn main() {
    let unit = ();
    println!("Size of unit type: {}", std::mem::size_of::<()>()); // 输出 0
}

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

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

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

(1) 标志类型(Marker Types)

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

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,用于表示无返回值的函数或某些操作的结果。例如:

fn do_nothing() {}

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

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

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 时,编译器会避免为它分配额外的空间。

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 函数来检查任何类型的大小:

use std::mem;

struct Empty;
struct Data {
    x: i32,
    y: i32,
}

fn main() {
    println!("Size of Empty: {}", mem::size_of::<Empty>()); // 输出 0
    println!("Size of Data: {}", mem::size_of::<Data>());   // 输出 8
    println!("Size of (): {}", mem::size_of::<()>());       // 输出 0
}

4. 注意事项

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

(1) 指针的行为

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

struct Empty;

fn main() {
    let e1 = Empty;
    let e2 = Empty;

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

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

#[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

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

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 只是一个没有任何字段的结构体:

struct Parent;

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

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

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

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

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

如何修复这个问题?

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

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

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

static INSTANCE: Parent = Parent {
	data: "Initial Data".to_string(), 
};  

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

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

struct Child {
    inner: Parent,
}

impl Deref for Child {
    type Target = Parent;

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

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

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.kler.cn/a/506824.html

相关文章:

  • 【Redis】Redis 集群中节点之间如何通信?
  • redhat安装docker 24.0.7
  • YOLOv10-1.1部分代码阅读笔记-build.py
  • 【25考研】西南交通大学软件工程复试攻略!
  • 【Linux】Mysql部署步骤
  • Elasticsearch:Jira 连接器教程第一部分
  • 【设计模式-结构型】装饰器模式
  • C++ union 联合(八股总结)
  • 微调神经机器翻译模型全流程
  • 紫光无人机AI飞控平台介绍
  • Mybatis-Plus:简介、入门案例
  • 【Excel】【VBA】双列排序:坐标从Y从大到小排列之后相同Y坐标的行再对X从小到大排列
  • 【matlab】matlab知识点及HTTP、TCP通信
  • npm发布工具包+使用
  • windows11下 podman-desktop 复制插件文件 到 RabbitMQ 容器内,并启用
  • 安全规约、脱敏规范、敏感逻辑的保护方案、防止 SQL 注入
  • 25/1/16 嵌入式笔记 STM32F108
  • C语言结构体漫谈:从平凡中见不平凡
  • 《基于深度学习的多色光度巡天项目天文目标检测框架》论文精读
  • 1 使用EMIO
  • 【Axure】配色库
  • 5、docker-compose和docker-harbor
  • 电池预测 | 第21讲 基于Gamma伽马模型结合EM算法和粒子滤波算法参数估计的锂电池剩余寿命预测
  • python中自动化playwright录制功能跳过繁琐的系统登录操作
  • Git在add的时候出现error: unable to index file 的问题,导致add失败的问题解决
  • 大数据就业前景及待遇如何?