暨 Repository 简介
引子Rust 在处理数据时表现第一好情况的是处理树状数据,第二好的情况是数据能够看作有向无环图,但是其中执行数据变化的修改脉络路径仍然能看作是树形,同样好的也还有粗粒度执行的可以看作是多读单写锁的协议下的数据处理,诸如此类。在这个舒适带之外,Rust工作的就没有那么好。虽然还能用,但是处理变得棘手,很多为了安全性和友好性准备的语言特征都不再能够协助你,使用体验变得不方便起来。这本书(指《Learn Rust With Entirely Too Many Linked Lists》)的目标之一就是探索(在Rust开发中)如何与双向链表这样的东西打交道。 双向链表是一个 Rust 在当前的设计中,让你不能写,或者至少不能轻易的不小心的写出来的结构。双向链表不只是具有不明确的所有权,它实际上具有的是循环的所有权,显然离 Rust 的舒适区域相去甚远。显然,最直接、也是在智力上最诚实的对这本书的回应就是:它深入地论断反对了在Rust中使用循环依赖数据。原则就是开发中“不要让数据循环依赖”,看看你能做到什么地步。这也是 Rust 设计的思路。我不认为我写的 Rust 程序中有很多的循环依赖数据,少数用到的地方都是使用封装好的库,或者是不透明的引用句柄(而不是 Rust 所有权系统跟踪着的原生引用)。
我觉得这段话总结的蛮好的,除了“不要让数据循环依赖”略显武断。在实际的业务开发中,在某些特定的场景之下,“不要让数据循环依赖”总是要让路于“让数据循环依赖变得可管理”。当然这段话的末尾实际上已经提到了解决方案——使用封装好的库(让别人使用某种机制来替你管理并封装起来),而别人实际实现的方法就是使用不透明的引用句柄。
最最常见的不透明的引用句柄其实就是原生指针。原生指针尺寸小,速度快。缺点也是显而易见的,它对bug几乎完全没有容灾性。遇到数据的不一致,甚至有时候只是业务逻辑上的逻辑错误导致的,就很可能要与SIGSEGV相伴了。如果是部署到用户手上的系统,喏,一个崭新的CVE可能就诞生了。
另外一种很常见的不透明引用句柄就是索引。索引尺寸小,速度也很好,具有最基本的容灾性——至少很容易就能保证不会访问到不存在的值。它最大的缺点是会丢失类型信息。的确在很多情况下,会想要擦除类型信息,但是在逻辑上想要找回类型信息的时候,单纯的索引显然会捉襟见肘。另外就是需要小心处理值删除引起其他值的索引偏移。最简单的办法就是让索引无法偏移,当然这也并不是全无代价的。
Repository
repository的其他功能,希望对大家有借鉴意义。
repository目前的最大的限制是要求其中所有的数据都符合core::any::Any这个特质,所以无法使用带有非'static引用类型的数据。这并不是什么大问题,如果真的需要,你可以将非'static类型的值留在Repo之外,使用某种方法建立对应关系,然后封装一层即可使用。另外就是我目前没有实现多线程版本。我自己是有用这个库试验性的写了一些东西,感觉至少对于单线程环境下的一般使用还是很方便的。
用 Repo 定义双向链表和节点
先上代码:
userepository::{Repo,EntityPtr,ErrorasRepoError};usecore::any::Any;struct LinkedList{repo: Repo,ends: Option>,EntityPtr>)>,len: usize,}struct Node{value: T,prev: Option>>,next: Option>>,}
这里用到了 repository 里的 Repo 这个类型,它是一块存储空间,你可以把它想像成类似 Vec 一样的东西,但是它可以放多种类型的数据,所以没有泛型约束。
在这里我们会用它来存储双向链表的节点,因为节点的值的类型是用户指定的 T ,所以用泛型指定。我们打算把Node放进Repo,这要求它符合Any特质约束,所以T也需要符合Any特质约束,写成T: Any。
将类型为 Node 的数据放入 Repo 会得到一个EntityPtr> 这是一个满足Copy的大小为usize两倍的不透明的引用句柄。你可以随意存储、序列化、传输。它的类型上记录了实际内容值的类型。我们在Node内,就是用它套上Option用来存储前一个节点和后一个节点的。
那么我想要访问实际的Node怎么办呢?Rust 分为共享借用和独占借用。如果ptr是一个EntityPtr>,那么获取共享借用和独占借用分别是:
// 获取共享借用letnode_ref=ptr.get_ref(&repo)?;// 获取独占借用letnode_mut=ptr.get_mut(&mutrepo)?;
因为EntityPtr是完全独立于Repo存在的,所以这一步有报错的可能:比如对应的值可能已经删掉了,之类的。这里用了问号做错误处理。
接下来就是实现双向链表的各种方法了。其实都比较简单,在这里不做赘述。
Repo 里的类型擦除
有时候我们需要把一堆不同类型的指针存在一起。这个时候EntityPtr会因为上面带了类型而做不到这一点。EntityPtr 上提供了entity_id()方法,获取到一个EntityId。它是一个只有一个usize大小的,不带类型标识的索引编号。从 EntityId 重新获取EntityPtr则是通过cast_ptr方法。
// 获取 ptr 对应的idletnode_id=ptr.entity_id(&repo)?;// 获取 id 对应的ptrletnode_ptr=node_id.cast_ptr::>(&repo);assert_eq!(node_ptr,Some(ptr));
Repo 里的预分配机制
有时候我们在构建数据结构时,它们存在循环引用,然而因为构建是逐个进行的,那么就会存在前一个值需要引用后一个值,但是后一个值还没有创建的情况。 repository 提供了预分配机制,允许你先为一个值构建EntityPtr,然后在稍后再加入实际对应的值数据。
举个例子:假如有个数据结构叫“文档”,存储“根节点”,而在节点上又需要存储对应的文档的引用。代码如下:
struct Doc{root_node: EntityPtr}struct Node{doc: EntityPtr}repo.transaction_preallocate(|tx|-> EntityPtr{letdoc_ptr=tx.preallocate::();letnode=Node{doc: doc_ptr};letnode_ptr=tx.repo_mut().insert(node);letdoc=Doc{root_node: node_ptr};// 只是演示,所以省去了错误处理,直接unwrap()。tx.init_preallocation(doc_ptr,doc).unwrap();doc_ptr});
好啦,这次就简单介绍到这,欢迎讨论。