Rust 中的数据布局--非正常大小的类型
非正常大小的类型
大多数的时候,我们期望类型在编译时能够有一个静态已知的非零大小,但这并不总是 Rust 的常态。
Dynamically Sized Types (DSTs)
Rust 支持动态大小的类型(DST):这些类型没有静态(编译时)已知的大小或者布局。从表面上看这有点离谱:Rust 必须知道一个东西的大小和布局,才能正确地进行处理。从这个角度上看,DST 不是一个普通的类型,因为它们没有编译时静态可知的大小,它们只能存在于一个指针之后。任何指向 DST 的指针都会变成一个包含了完善 DST 类型信息的胖指针(详情见下方)。
Rust 暴露了两种主要的 DST 类型:
Trait 对象代表某种类型,实现了它所指定的 Trait。确切的原始类型被删除,以利于运行时的反射,其中包含使用该类型的所有必要信息的 vtable。补全 Trait 对象指针所需的信息是 vtable 指针,被指向的对象的运行时的大小可以从 vtable 中动态地获取。
一个 slice 只是一些只读的连续存储——通常是一个数组或Vec
。补全一个 slice 指针所需的信息只是它所指向的元素的数量,指针的运行时大小只是静态已知元素的大小乘以元素的数量。
结构实际上可以直接存储一个 DST 作为其最后一个字段,但这也会使它们自身成为一个 DST:
// 不能直接存储在栈上
struct MySuperSlice {
info: u32,
data: [u8],
}
如果这样的类型没有方法来构造它,那么它在很大程度上来看是没啥用的。目前,唯一支持的创建自定义 DST 的方法是使你的类型成为泛型,并执行非固定大小转换(unsizing coercion):
struct MySuperSliceable<T: ?Sized> {
info: u32,
data: T,
}
fn main() {
let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
info: 17,
data: [0; 8],
};
let dynamic: &MySuperSliceable<[u8]> = &sized;
// 输出:"17 [0, 0, 0, 0, 0, 0, 0, 0]"
println!("{} {:?}", dynamic.info, &dynamic.data);
}
(是的,自定义 DST 目前仅仅是一个基本半成品的功能。)
零大小类型 (ZSTs)
Rust 也允许类型指定他们不占空间:
struct Nothing; // 无字段意味着没有大小
// 所有字段都无大小意味着整个结构体无大小
struct LotsOfNothing {
foo: Nothing,
qux: (), // 空元组无大小
baz: [u8; 0], // 空数组无大小
}
就其本身而言,零尺寸类型(ZSTs)由于显而易见的原因是相当无用的。然而,就像 Rust 中许多奇怪的布局选择一样,它们的潜力在通用语境中得以实现。在 Rust 中,任何产生或存储 ZST 的操作都可以被简化为无操作(no-op)。首先,存储它甚至没有意义——它不占用任何空间。另外,这种类型的值只有一个,所以任何加载它的操作都可以直接凭空产生它——这也是一个无操作(no-op),因为它不占用任何空间。
这方面最极端的例子之一是 Set 和 Map。给定一个Map<Key, Value>
,通常可以实现一个Set<Key>
,作为Map<Key, UselessJunk>
的一个薄封装。在许多语言中,这将需要为无用的封装分配空间,并进行存储和加载无用封装的工作,然后将其丢弃。对于编译器来说,证明这一点是不必要的,是一个困难的分析。
然而在 Rust 中,我们可以直接说Set<Key> = Map<Key, ()>
。现在 Rust 静态地知道每个加载和存储都是无用的,而且没有分配有任何大小。其结果是,单例化的代码基本上是 HashSet 的自定义实现,而没有 HashMap 要支持值所带来的开销。
安全的代码不需要担心 ZST,但是不安全的代码必须小心没有大小的类型的后果。特别是,指针偏移是无操作的,而分配器通常需要一个非零的大小。
请注意,对 ZST 的引用(包括空片),就像所有其他的引用一样,必须是非空的,并且适当地对齐。解引用 ZST 的空指针或未对齐指针是未定义的行为,就像其他类型的引用一样。
空类型
Rust 还允许声明不能被实例化的类型。这些类型只能在类型层讨论,而不能在值层讨论。空类型可以通过指定一个没有变体的枚举来声明:
enum Void {} // 没有变体的类型 = 空类型
空类型甚至比 ZST 更加边缘化。空类型的主要作用是为了让某个类型不可达。例如,假设一个 API 需要在一般情况下返回一个结果,但一个特定的情况实际上是不可能的。实际上可以通过返回一个Result<T, Void>
来在类型级别上传达这个信息。API 的消费者可以放心地 unwrap 这样一个结果,因为他们知道这个值在本质上不可能是Err
,因为这需要提供一个Void
类型的值。
原则上,Rust 可以基于这个事实做一些有趣的分析和优化,例如,Result<T, Void>
只表示为T
,因为Err
的情况实际上并不存在(严格来说,这只是一种优化,并不保证,所以例如将一个转化为另一个仍然是 UB)。
比如以下的例子,曾经是可以编译成功的:
enum Void {}
let res: Result<u32, Void> = Ok(0);
// 不存在 Err 的情况,所以 Ok 实际上永远都能匹配成功
let Ok(num) = res;
但现在,已经不让这么玩儿了。
关于空类型的最后一个微妙的细节是,构造一个指向它们的原始指针实际上是有效的,但对它们的解引用是未定义行为,因为那是没有意义的。
我们建议不要用*const Void
来模拟 C 的void*
类型。很多人之前这样做,但很快就遇到了麻烦,因为 Rust 没有任何安全防护措施来防止用不安全的代码来实例化空类型,如果你这样做了,就是未定义行为。因为开发者有将原始指针转换为引用的习惯,而构造一个&Void
也是未定义行为,所以这尤其成问题。
*const ()
(或等价物)对void*
来说效果相当好,可以做成引用而没有任何安全问题。它仍然不能阻止你试图读取或写入数值,但至少它可以编译成一个 no-op 而不是 UB。
外部类型
有一个已被接受的 RFC 来增加具有未知大小的适当类型,称为 extern 类型,这将让 Rust 开发人员更准确地模拟像 C 的void*
和其他“声明但从未定义”的类型。然而,截至 Rust 2018,该功能在size_of_val::<MyExternType>()
应该如何表现方面遇到了一些问题。
Rust 中的数据布局--非正常大小的类型的更多相关文章
- Rust 中的数据布局--可选的数据布局
Rust 允许你指定不同于默认的数据布局策略,并为你提供了不安全代码指南. repr(C) 这是最重要的"repr".它的意图相当简单:做 C 所做的事.字段的顺序.大小和对齐方式 ...
- Rust 中的数据布局-repr
repr(Rust) 首先,所有类型都有一个以字节为单位的对齐方式,一个类型的对齐方式指定了哪些地址可以用来存储该值.一个具有对齐方式n的值只能存储在n的倍数的地址上.所以对齐方式 2 意味着你必须存 ...
- 在 sql server 中,查询 数据库的大小 和 数据库中各表的大小
其实本来只想找一个方法能查询一下 数据库 的大小,没想到这个方法还能查询数据库中 各个数据表 的大小,嗯,挺好玩的,记录一下. MSDN资料:https://msdn.microsoft.com/zh ...
- 借助 SIMD 数据布局模板和数据预处理提高 SIMD 在动画中的使用效率
原文链接 简介 为发挥 SIMD1 的最大作用,除了对其进行矢量化处理2外,我们还需作出其他努力.可以尝试为循环添加 #pragma omp simd3,查看编译器是否成功进行矢量化,如果性能有所提升 ...
- c++继承中的内存布局
今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化 译 译者前言 一个C ...
- 关于SWT中的GridLayout布局方式
GridLayout 布局的功能非常强大,也是笔者常用的一种布局方式.GridLayout是网格式布局,它把父组件分成一个表格,默认情况下每个子组件占据一个单元格的空间,每个子组件按添加到父组件的顺序 ...
- Android中的数据存储
Android中的数据存储主要分为三种基本方法: 1.利用shared preferences存储一些轻量级的键值对数据. 2.传统文件系统. 3.利用SQLite的数据库管理系统. 对SharedP ...
- ELF文件数据布局探索(1)
作为一名Linux小白,第一次看到a.out这个名字,感觉实在是奇怪,搜了一下才知道这是编译器输出的默认可执行文件名 然后vi一下,哇,各种乱码,仔细看看,发现了三个清晰的字符ELF.继续搜索, 第一 ...
- C++继承 派生类中的内存布局(单继承、多继承、虚拟继承)
今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化 译 译者前言 一个C ...
随机推荐
- 一、ES6基础
一.ECMAScript和JavaScript关系 JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标 准,但 ...
- Java、Python语法区别,不断更新
基本语句.文件方面 Java中的字符是单引号,字符串是双引号:Python则是单双都可以 Java语句结束有分号;,Python没有(写分号也正确) Java中程序执行需要有main函数,Python ...
- Python:读取二进制文件时使用print输出\x
问题: 有二进制文件,通过open打开和read()读入并输出时,输出为\x十六进制编码,不能正确显示其具体代表的字符 with open(r'C:\Users\Le\Desktop\Test\tki ...
- redis缓存雪崩和缓存穿透
缓存雪崩:由于原有的缓存过期失效,新的缓存还没有缓存进来,有一只请求缓存请求不到,导致所有请求都跑去了数据库,导致数据库IO.内存和CPU眼里过大,甚至导致宕机,使得整个系统崩溃. 解决思路:1,采用 ...
- JZ-030-连续子数组的最大和
连续子数组的最大和 题目描述 HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学.今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和, 当向量全为正数的时候,问题很 ...
- FlinkX
FlinkX的安装与简单使用 目录 FlinkX的安装与简单使用 FlinkX的安装 FlinkX的简单使用 MySQLToHDFS MySQLToHive MySQLToHBase MySQLToM ...
- think php jq ajax删除
表单展示页面 <!doctype html> <html lang="en"> <head> <meta charset="UT ...
- angularJs 指令的用法
<!DOCTYPE html><html ng-app='app'><!--A attribute属性:当做属性来使用<div xingoo></div ...
- MySQL 字符集相关
为了支持各个国家的不同语言,MySQL 从4.0 版本开始支持了很多种字符集,且每种字符集支持了 N 多种排序规则.我们可以在建表的时候指定字符集的排序规则,不指定时会有一个默认规则. 字符集和排序规 ...
- 使用Spring Data ElasticSearch+Jsoup操作集群数据存储
使用Spring Data ElasticSearch+Jsoup操作集群数据存储 1.使用Jsoup爬取京东商城的商品数据 1)获取商品名称.价格以及商品地址,并封装为一个Product对象,代码截 ...