Rust死灵书 - Rust高级与非安全程序设计 (rust官方)(Z-Library)
Author: rust官方
RUST
本书将深入挖掘Rust非安全(unsafe)编程中的一些必要但是又可怕的细节。由于此类问题天然的恐怖,本书散发出的不可描述的恐惧之力,极可能将你的神经彻底撕成千万个绝望的碎片。 如果你学会了rust还是感觉处处受限,应该看这本书,如果一个算法因为所有权很难实现,你应该看它
📄 File Format:
PDF
💾 File Size:
1.8 MB
6
Views
0
Downloads
0.00
Total Donations
📄 Text Preview (First 20 pages)
ℹ️
Registered users can read the full content for free
Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.
📄 Page
1
(This page has no text content)
📄 Page
2
目 录 致谢 简介 初识安全与非安全代码 安全与非安全代码的交互方式 非安全Rust能做什么 编写非安全代码 数据布局 repr(Rust) 类型中的奇行种 其他repr 所有权 引用 别名 生命周期 生命周期的局限 省略生命周期 无界生命周期 高阶trait边界 子类型和变性 Drop检查 PhantomData(幽灵数据) 分解借用 类型转换 强制类型转换 点操作符 显式类型转换 变形 未初始化内存 安全方式 Drop标志 非安全方式 基于所有权的资源管理 构造函数 析构函数 泄露 展开 - 2 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
3
异常安全性 污染 并发 竞争 Send和Sync 原子操作 实现Vec 布局 内存分配 push和pop 回收资源 DeRef 插入和删除 IntoIter RawVec Drain 处理零尺寸类型 最终代码 实现Arc和Mutex FFI - 3 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
4
致谢 当前文档 《Rust死灵书 - Rust高级与非安全程序设计》 由 进击的皇虫 使用 书栈 (BookStack.CN) 进行构建,生成于 2019-06-07。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工 具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如果 您在阅读文档获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手,将知 识准确、高效且有效地传递给每一个人。 同时,如果您在日常工作、生活和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈 (BookStack.CN) ,为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换 代的步伐。 内容来源:tjxing https://github.com/tjxing/rustonomicon_zh-CN 文档地址:http://www.bookstack.cn/books/rustonomicon_zh-CN 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的 读者,因为我们都将成为知识的传承者。 致谢 - 4 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
5
原文链接: https://doc.rust-lang.org/nomicon/ 我一直期待的程序代码并未出现,取而代之的竟是这令人战栗的黑暗与不可名状的孤独。我看见了!那个让所有 人都噤声不语的恐怖事实,那个不可言说的秘密中的秘密——这个精心构建的Rust语言,其实并不像它最初看起 来那般坚固不朽。事实上,它竟然是非安全的,它的身躯散发着古怪的气味,滋生着诡异的寄生生物。而我,对 这一切束手无策,因为它们都是在编译期发生的。 本书将深入挖掘Rust非安全(unsafe)编程中的一些必要但是又可怕的细节。由于此类问题天然的恐 怖,本书散发出的不可描述的恐惧之力,极可能将你的神经彻底撕成千万个绝望的碎片。 如果你仍然期待着拥有一个长期且快乐的Rust编程生涯,那么现在就转身离开,彻底忘掉你曾经见到过 这本书——你并不会感到生活有什么缺憾。但是,如果你计划编写非安全代码——或者仅仅是想探究一下这 门语言的内在秘密——本书将给你许多有用的信息。 与《Rust程序设计》那本书不同,本书假设你具备一定的基础知识。特别是你应该已经熟练掌握了基本 的系统编程和Rust语言。要是还没有的话,请考虑先读这本书。我们并不假设你一定去读了,也会在适 当的时候复习一下相关的基础知识。你可以跳过上面那本书直接阅读本书,但要了解我们并不会把每一 个知识点都从头讲起。 我们将涉及到异常安全(exception-safety),指针别名(pointer aliasing),内存模型 (memory model),编译器和硬件实现的细节,甚至还有一些类型理论(type-theory)。还会大费周 章地处理一些原本不该有人关注的边界场景,因为当我们敲出unsafe几个字的时候,这些场景一下子 就变得特别重要了。 我们还将花费大量时间讨论程序关注的各种不同的安全保证机制。 Rust死灵书 黑魔法 之 Rust高级与非安全程序设计 注意:本文档讨论了诸多Rust尚未稳定的特性,可能包含 一些错误或者过时的信息。 简介 - 5 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
6
原文链接:https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html 大家都希望可以彻底屏蔽代码底层实现的细节。又有谁愿意关心“一个空的元组占用多少内存”这种破 事?可惜的是,有时候这些破事却很重要,我们不得不去关注它。开发人员关注实现细节,大部分情况 是为了性能优化。但更主要的是,当我们与硬件、操作系统或者其他语言直接打交道的时候,这些细节 往往是正确与否的关键。 当使用某种安全编程语言的过程中遇到了处理底层实现的需求时,程序员通常有三种选择: 修改代码让编译器或者运行时环境做相关优化 采取某些古怪、繁琐的奇技淫巧以实现功能需求 使用另一种可以处理底层细节的语言重写代码 对于最后一个选项,程序员通常会选择C语言。某些系统也只对外暴漏了C的接口。 然而,C在使用中往往过于不安全(虽然有时是出于合理的原因)。尤其是在与其他语言交互的过程 中,这种不安全性还会被放大。C和与其交互的语言必须时刻小心地确认对方的行为,以防踩到舞伴的 脚趾头。 那么这和Rust有什么关系呢? 嗯……不同于C,Rust是一种安全的编程语言。 但是,和C相同的是,Rust是一种非安全的编程语言。 更准确地说,Rust是一种同时包含安全和非安全特性的编程语言。 Rust可以被看作两种编程语言的结合体:安全Rust和非安全Rust。顾名思义,安全Rust是安全的, 而不安全Rust……嗯……是不安全的。不安全Rust允许我们做一些非常不安全的事情——就是那些Rust的 创造者们求我们别去做可我们偏要做的事情。 安全Rust是一种真正的安全编程语言。如果你所有的代码都是用安全Rust写的,你永远也无需担心类 型安全和内存安全,无需费神处理悬垂指针、释放后引用(use-after-free),或者其他各种未定义 的行为。 标准库也提供了相当多的工具,帮助你用符合安全Rust语言规范的方式创建高性能的应用和库。 不过,也许是时候谈论一下另一种语言了。也许你正在写一种标准库没有覆盖到的底层抽象;也许你正 在开发标准库(存粹使用Rust语言);也许你要做一些类型系统不能理解的事情,还要胡乱摆弄各种字 节码。也许,你需要非安全Rust了。 非安全Rust和安全Rust的语法规则完全相同,只不过它允许你做一些另外的不安全的行为(下一节再 初识安全与非安全代码 初识安全与非安全代码 - 6 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
7
告诉你都包括什么)。 分离安全与非安全Rust的价值在于,我们既可以享受像C那样的非安全语言的好处——也就是对底层实现 细节的控制,又不用处理C与其他安全语言集成时遇到的种种问题。 不过还是会遇到一些问题。最明显的。我们必须非常了解类型系统的全部默认要求,并在每次与非安全 代码交互的时候检查它们。这也是本书的目的:教给你这些要求以及如何处理它们。 初识安全与非安全代码 - 7 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
8
原文链接:https://doc.rust-lang.org/nomicon/safe-unsafe-meaning.html 安全与非安全代码之间的关系是什么?它们又如何交互呢? 安全与非安全代码是靠 unsafe 关键字分离的,它扮演着两种语言之间接口的角色。这也是我们理直 气壮地声称安全Rust是安全的原因:所有的非安全代码都被 unsafe 隔离在外。只要你愿意,你甚至 可以在代码根部添加 #![forbid(unsafe_code)] 以保证你只会写安全的代码。 unsafe 关键字有两层含义:声明代码中存在编译器无法检查的安全规范,同时声明开发者会自觉遵 守相关规范而不会主动破坏它。 你可以使用关键字 unsafe 表明函数和trait的声明中存在不受编译器检查的规范。对于函 数, unsafe 意味着调用函数的开发者必须查阅函数的文档以确保他们的用法符合函数的安全要求。 而对于trait的声明, unsafe 意味着实现trait的开发者必须查阅trait的文档以确保trait的实 现符合其安全要求。 你可以给一个代码块添加 unsafe 关键字,声明块中的所有代码都已经人工检查过符合相关规范。比 如,传递给 slice::get_unchecked 的索引值都没有越界。 你也可以在实现一个trait时使用 unsafe 关键字,声明实现符合trait的安全规范。比如,实 现 Send 的类型可以绝对安全地转移(move)进另一个线程中。 标准库也有一些非安全函数,包括: slice::get_unchecked ,可接受不受检查的索引值,也就是存在内存安全机制被破坏的可能 mem::transmute ,将值重新解析成另一种类型,即允许随意绕过类型安全机制的限制(详情参 考类型转换) 所有指向确定大小类型(sized type)的裸指针都有 offset 方法,当传入的偏移量越界时将导 致未定义行为(Undefined Behavior)。 所有FFI(Foreign Function Interface)函数都是 unsafe 的,因为其他的语言可以做各 种的操作而Rust编译器无法检查它。 在Rust 1.0中,有两个非安全的trait: Send 是一个标志trait(即没有任何方法的trait),承诺所有的实现都可以安全地发送 (move)到另一个线程。 Sync 也是一个标志trait,承诺线程可以通过共享的引用共享它的实现。 许多Rust标准库其实内部也使用了非安全Rust。这些库的实现方法都经过了严苛的人工检查,所以这 些基于非安全Rust实现的安全Rust接口依然可以认为是安全的。 安全与非安全代码的交互方式 安全与非安全代码的交互方式 - 8 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
9
这种代码 隔离的存在说明了安全Rust的一个基本特征: 无论如何,安全Rust代码都不能导致未定义行为 可以看出,安全和非安全的Rust之间存在一种不对称的信任关系。安全Rust必须无条件信任非安全 Rust,假定所有与之打交道的非安全代码都是正确的。反过来,非安全Rust却要谨慎对待安全Rust的 代码。 举个例子,Rust有 PartialOrd 和 Ord 两个trait,区别在于前者仅仅表示可以被比较的类型, 而后者则表示实现了完整顺序(total ordering)的类型(也就是比较的机制更符合直觉)。 BTreeMap 只有在键是完整顺序时才能正常工作,所以它要求它的键必须实现 Ord 。但 是, BTreeMap 的内部实现却依赖于非安全Rust代码。因为如果 Ord 的实现本身是错误的(尽管 代码是安全的)将导致未定义行为,所以 BTreeMap 内部的非安全代码必须对那些实际上没有做到完 整顺序的 Ord 保持足够的鲁棒性——虽然完整顺序本身是我们选择 Ord 的唯一理由。 非安全Rust不能简单地信任安全Rust都是正确的。也就是说,如果你传入到 BTreeMap 的值不具备 完整顺序, BTreeMap 的行为将会完全混乱。它仅仅能保证不会产生未定义行为罢了。 有人或许会问,如果 BTreeMap 不能因为 Ord 是安全的就信任它,那为什么 BTreeMap 可以信任 其他的安全代码?比如, BTreeMap 依赖integer和slice的正确实现。那些不也是安全的代码吗? 区别之一是范围。当 BTreeMap 依赖于integer和slice时,它是依赖于某种特定的实现,其收益和 风险是可以评估的。依赖integer和slice的风险其实几乎为0,因为如果连它们都是错误的话,那么 所有的代码都不可能正确了。而且,它们和 BTreeMap 是由相同的开发者维护的,也比较容易配合。 而反过来, BTreeMap 的键类型是一个范型。信任它意味着要信任过去、现在和未来的所有 的 Ord 的实现。这种风险就很高了:来自世界某个角落的路人甲可能在实现 Ord 时不小心犯了一 个错误,或者他可能觉得代码“差不多没什么问题”就贸然声称它实现了完整排序。 BTreeMap 必须时 刻准备着面对这些情况。 上述逻辑同样适用于是否应该信任外部传递的闭包。 非安全trait的出现就是为了解决这一类不受限的信任问题。 BTreeMap 理论上可以要求键实现一个 新的叫做 UnsafeOrd 的trait,而不是现在的 Ord 。代码可能像这样 1. use std::cmp::Ordering; 2. 3. unsafe trait UnsafeOrd { 4. fn cmp(&self, other: &Self) -> Ordering; 5. } 接下来,一个类型要使用 unsafe 关键字实现 UnsafeOrd ,表明其实现符合trait要求的各种安全 规范。这时, BTreeMap 的内部就可以合理地信任键的类型对于 UnsafeOrd 的实现是正确的。如 安全与非安全代码的交互方式 - 9 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
10
果真的出错了,这个锅将由实现非安全trait的开发者来背,与Rust自身的安全机制并不冲突。 一个trait是否应该标志为 unsafe 是API设计上的选择。Rust通常会尽量避免这么做,因为它会导 致非安全Rust的滥用,这并不是设计者们希望看到的。 Send 和 Sync 被标识为非安全是因为线程 安全性是一个底层特性,非安全代码不太可能有效地检查它,并不像检查 Ord 的错误实现那样容易。 你也可以根据类似的标准判断是否要把你自己的trait标为 unsafe 。如果让安全代码去检查trait 实现的正确性不太现实,那么把trait标为 unsafe 就是合理的。 顺便说一下, Send 和 Sync 是会被各种类型自动实现的,只要这种实现可以被证明是安全的。如 果一种类型其所有的值的类型都实现了 Send ,它本身就会自动实现 Send ;如果一种类型其所有 的值的类型都实现了 Sync ,它本身就会自动实现 Sync 。将它们设为 unsafe 实际减少了非安 全代码的滥用。 安全Rust和非安全Rust各有所长。安全Rust被设计成尽可能地方便易用,而使用非安全Rust不仅要 投入更多的精力,还要格外地小心。本书接下来的内容主要讨论那些需要小心的点,以及非安全Rust必 须满足的规范。 安全与非安全代码的交互方式 - 10 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
11
原文链接:https://doc.rust-lang.org/nomicon/what-unsafe-does.html 非安全Rust比安全Rust可以多做的事情只有以下几个: 解引用裸指针 调用非安全函数(包括C语言函数,编译器内联函数,还有直接内存分配等) 实现非安全trait 访问或修改可变静态变量 就这些。这些操作被归为非安全的,是因为使用得不正确就会导致可怕的未定义行为。一旦触发了未定 义行为,编译器就可以放飞自我,肆意破坏你的程序。切记,一定不能给未定义行为任何的机会。 与C不同,Rust充分限制了可能出现的未定义行为的种类。语言核心只需要防止这几种行为: 解引用null指针,悬垂指针,或者未赋值的指针 读取未初始化的内存 破坏指针混淆规则 创建非法的基本类型: 悬垂引用与null引用 空的 fn 指针 0和1以外的 bool 类型值 未定义的枚举类型的项 在[0x0,0xD&FF]和[0xE000, 0x10FFFF]以外的 char 类型值 非utf-8编码的 str 不谨慎地调用其他语言 数据竞争 只有这些。Rust语言自身可以导致未定义行为的操作就只有这些。当然,非安全函数和trait可以声明 自己专有的安全规范,要求开发者必须遵守以避免未定义行为。比如,allocator API声明回收一段 未分配的内存是未定义行为。 但是,违背这些专有的规范通常也只是间接地触发上面列出的行为。另外,编译器内联函数也可能引入 一些规则,一般是针对代码优化的假设条件。比如,Vec和Box使用的内联函数要求传入的指针永远不 能为null。 Rust对于一些模糊的操作则通常比较宽容。Rust会认为下列操作是安全的: 死锁 竞争条件 内存泄漏 非安全Rust能做什么 非安全Rust能做什么 - 11 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
12
调用析构函数失败 整型值溢出 终止程序 删除产品数据库 当然,有以上行为的程序极有可能就是错误的。Rust提供了一系列的工具减少这种事情的发生,但是完 全地杜绝它们其实是不现实的。 非安全Rust能做什么 - 12 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
13
原文链接:https://doc.rust-lang.org/nomicon/working-with-unsafe.html Rust通常要求我们明确限制非安全Rust代码的作用域。可是,现实情况其实要更复杂一些。举个例 子,看一下下面的代码: 1. fn index(idx: usize, arr: &[u8]) -> Option<u8> { 2. if idx < arr.len() { 3. unsafe { 4. Some(*arr.get_unchecked(idx)) 5. } 6. } else { 7. None 8. } 9. } 这个函数是安全和正确的。我们检查了索引值有没有越界。如果没有,就从数组中用不安全的方式取出 对应的值。然而,哪怕是这么简单的一个函数,unsafe代码块的范围也不是绝对明确的。想象一下, 如果把 < 改成 <= : 1. fn index(idx: usize, arr: &[u8]) -> Option<u8> { 2. if idx <= arr.len() { 3. unsafe { 4. Some(*arr.get_unchecked(idx)) 5. } 6. } else { 7. None 8. } 9. } 这段程序就有潜在的问题了,但我们其实只修改了安全代码的部分。这是安全机制的一个根本性问题: 非本地性。意思是,非安全代码的稳定性其实依赖于另一些“安全”代码的状态。 是否进入非安全代码块,并不受其他部分代码正确性的影响,从这个角度看安全机制是模块化的。比 如,是否对一个slice进行不安全索引,不受slice是不是null或者是不是包含未初始化的内存这些事 情的影响。但是,由于程序本身是有状态的,非安全操作的结果实际依赖于其他部分的状态,从这个角 度看安全机制又是非模块化的。 在处理持久化状态时,非本地性带来的问题就更加明显了。看一下 Vec 的一个简单实现: 编写非安全代码 编写非安全代码 - 13 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
14
1. use std::ptr; 2. 3. // 注意:这个定义十分简单。参考实现Vec的章节 4. pub struct Vec<T> { 5. ptr: *mut T, 6. len: usize, 7. cap: usize, 8. } 9. 10. // 注意:这个实现未考虑大小为0的类型。参考实现Vec的章节 11. impl<T> Vec<T> { 12. pub fn push(&mut self, elem: T) { 13. if self.len == self.cap { 14. // 与例子本身无关 15. self.reallocate(); 16. } 17. unsafe { 18. ptr::write(self.ptr.offset(self.len as isize), elem); 19. self.len += 1; 20. } 21. } 22. } 这段代码很简单,便于审查和修改。现在考虑给它添加一个新的方法: 1. fn make_room(&mut self) { 2. // 增加容量 3. self.cap += 1; 4. } 这段代码是100%的安全Rust但是彻底的不稳定。改变容量违反了Vec的不变性( cap 表示分配给 Vec的空间大小)。Vec的其他部分并不会保护它,我们只能信任它的值是正确的,因为本来没有修改 它的方法。 因为代码逻辑依赖于struct的某个成员的不变性,那段 unsafe 的代码不仅仅污染了它所在的函 数,它还污染了整个module。一般来说,只有在一个私有的module里非安全代码才可能是真正安全 的。 上面的改动其实是可以正常工作的。 make_room 方法并不会导致Vec的问题,因为我们没有设置它为 public。只有定义这个方法的module可以调用它。同时, make_room 直接访问了Vec的私有成 员,所以它也只能在Vec所在的module内使用。 编写非安全代码 - 14 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
15
这允许我们基于一些复杂的不变性写一些绝对安全的抽象。在考虑安全Rust和非安全Rust的关系时, 这一点非常重要。 我们已经了解了非安全代码必须信任一部分安全代码,但是不应该信任所有的安全代码。出于相似的原 因,私有成员的限制对于非安全代码很重要:我们不需要无条件信任世界上所有的安全代码并且任由他 们搞乱我们的可信任状态。 安全机制万岁! 编写非安全代码 - 15 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
16
原文链接:https://doc.rust-lang.org/nomicon/data.html 底层编程经常需要关注数据布局。它非常重要,而且会影响这门语言的方方面面。所以我们将从Rust中 数据的表示方式开始讨论。 Rust中的数据表示 数据布局 - 16 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
17
原文链接:https://doc.rust-lang.org/nomicon/repr-rust.html 首先,每种类型都有一个数据对齐属性(alignment)。一种类型的对齐属性决定了哪些内存地址可以 合法地存储该类型的值。如果对齐属性是n,那么它的值的存储地址必须是n的倍数。所以,对齐属性2 表示值只能存储在偶数地址里,1表示值可以存储在任何的地方。对齐属性最小为1,并且永远是2的整 数次幂。虽然不同平台的行为可能会不同,但大部分情况下基础类型都是按照它的类型大小对齐的。特 别的是,在x86平台上 u64 和 f64 都是按照32位对齐的。 一种类型的大小都是它对齐属性的整数倍,这保证了这种类型的值在数组中的偏移量都是其类型尺寸的 整数倍,可以按照偏移量进行索引。需要注意的是,动态尺寸类型的大小和对齐可能无法静态获取。 Rust有如下几种复合类型: 结构体(带命名的复合类型 named product types) 元组(匿名的复合类型 anonymous product types) 数组(同类型数据集合 homogeneous product types) 枚举(带命名的标签联合体 named sum types — tagged unions) 如果枚举类型的变量没有关联数据,它就被称之为无成员枚举。 结构体的对齐属性等于它所有成员的对齐属性中最大的那个。Rust会在必要的位置填充空白数据,以保 证每一个成员都正确地对齐,同时整个类型的尺寸是对齐属性的整数倍。例如: 1. struct A { 2. a: u8, 3. b: u32, 4. c:u16, 5. } 在对齐属性与类型尺寸相同的平台上,这个结构体会按照32位对齐。整个结构体的类型尺寸是32位的整 数倍。它实际会转变成这样: 1. struct A { 2. a: u8, 3. _pad1: [u8; 3], // 为了对齐b 4. b: u32, 5. c: u16, 6. _pad2: [u8; 2], // 保证整体类型尺寸是4的倍数 7. // (译注:原文就是“4的倍数”,但似乎“32的倍数”才对) 8. } repr(Rust) repr(Rust) - 17 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
18
这里所有的类型都是直接存储在结构体中的,成员类型和结构体之间没有其他的中介。这一点和C是一 样的。但是除了数组以外(数组的子类型总是按顺序紧密排列),其他的复合类型的数据分布规则并不 一定是固定不变的。对于下面两个结构体定义: 1. struct A { 2. a: i32, 3. b: u64, 4. } 5. 6. struct B { 7. a: i32, 8. b: u64, 9. } Rust可以保证A的两个实例的数据布局是完全相同的。但是Rust目前不保证A的实例和B的实例有着一 样的数据填充和成员顺序,虽然看起来他们似乎就应该是一样的才对。 对于上面的A和B来说,这一点大概显得莫名其妙。可是当Rust要处理更复杂的数据布局问题时,它就 变得很有必要了。 例如,对于这个结构体: 1. struct Foo<T, U> { 2. count: u16, 3. data1: T, 4. data2: U, 5. } 现在考虑范型 Foo<u32, u16> 和 Foo<u16, u32> 。如果Rust按照代码中指定的顺序布局结构体 成员,那么它就必须填充数据以符合对齐规则。所以,如果Rust不改变成员顺序的话,他们实际上会变 成这样: 1. struct Foo<u16, u32> { 2. count: u16, 3. data1: u16, 4. data2: u32, 5. } 6. 7. struct Foo<u32, u16> { 8. count: u16, 9. _pad1: u16, 10. data1: u32, repr(Rust) - 18 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
19
11. data2: u16, 12. _pad2: u16, 13. } 后者显然太浪费内存了。所以,内存优化原则要求不同的范型可以有不同的成员顺序。 枚举把这件事搞得更复杂了。举一个简单的枚举类型为例: 1. enum Foo { 2. A(u32), 3. B(u64), 4. C(u8), 5. } 它的布局会是这样: 1. struct FooRepr { 2. data: u64, // 根据tag的不同,这一项可以为u64,u32,或者u8 3. tag: u8, // 0 = A, 1 = B, 2 = C 4. } 这也确实就是一般情况下枚举的布局方式。 但是,在很多情况下这种表达方式并不是效率最高的。一个典型场景就是Rust的“null指针优化”:如 果一个枚举类型只包含一个单值变量(比如 None )和一个(级联的)非null指针变量(比 如 &T ),那么tag其实是不需要的,因为那个单值变量完全可以用null指针来表示。所 以, size_of::<Option<&T>>() == size_of::<&T>() ,这个比较的结果是正确的。 Rust中的许多类型都包含或者本身就是非null指针,比 如 Box<T> , Vec<T> , String , &T 以及 &mut T 。同样的,你或许也能想到,对于级 联的枚举类型,Rust会把多个tag变量合并为一个,因为它们本来就只有几个有限的可能取值。大体说 来,枚举类型会运用复杂的算法确定各种级联类型的二进制表达方法。因为这件事很重要,我们把枚举 的问题留到后面讨论。 repr(Rust) - 19 -本文档使用 书栈(BookStack.CN) 构建
📄 Page
20
原文链接:https://doc.rust-lang.org/nomicon/exotic-sizes.html 大部分情况下,我们考虑的都是拥有固定的正数尺寸的类型。但是,并非所有类型都是这样。 Rust支持动态尺寸类型,即不能静态获取尺寸或对齐属性的类型。乍一看,这事有点荒谬——Rust必须 知道一种类型的大小和对齐方式才能正确地使用它啊!从这一点来看,DST不是一个普通的类型。由于 类型大小是未知的,只能通过某种指针来访问它。所以,一个指向DST的指针是一个“胖”指针,它包含 指针本身和一些额外的信息(具体请往下看)。 语言提供了两种主要的DST:trait对象和slice。 trait对象表示实现了某种指定trait的类型。具体的类型被擦除了,取而代之的是运行期的一个虚函 数表,表中包含了使用这种类型所有必要的信息。这就是trai对象的额外信息:一个指向虚函数表的指 针。 slice简单来说是一个连续存储结构的视图——最典型的连续存储结构是数组或 Vec 。slice对应的 额外信息就是它所指向元素的数量。 结构体可以在最后的位置上保存一个DST,但是这样结构体本身也就变成了一个DST。 1. // 不能直接存储在栈上 2. struct Foo { 3. info: u32, 4. data: [u8], 5. } Rust实际允许一种类型不占用内存空间: 1. struct Foo; // 没有成员 = 没有尺寸 2. 3. // 所有成员都没有尺寸 = 没有尺寸 4. struct Baz { 5. foo: Foo, 6. qux: (), // 空元组没有尺寸 7. baz: [u8; 0], // 空数组没有尺寸 类型中的奇行种 动态尺寸类型(DST, Dynamically Sized Type) 零尺寸类型(ZST, Zero Sized Type) 类型中的奇行种 - 20 -本文档使用 书栈(BookStack.CN) 构建
The above is a preview of the first 20 pages. Register to read the complete e-book.
Recommended for You
Loading recommended books...
Failed to load, please try again later