你们中的一些人可能知道我辞去了工作,去做一个小的文本编辑器,只是为了好玩。不幸的是,在这样做的过程中,我被自我调整计算的挑战性问题分散了注意力。不幸的是,要以高性能的方式解决这个问题,您需要非常快速的图形,这是铁锈中的一个臭名昭著的问题。为了获得真正快速的图表,我发现了一种受诅咒的技术,它利用了Rust的生命周期规则和更高排名的特征界限,这就是我今天想要谈论的。除了它对差异的可疑解释之外,我很自豪地说,这篇博客帖子代表了我在刮牦牛方面的一项宝贵的成就,它的深度是4层,也许是5层。值得注意的是,我甚至还在使用Rust--这个深层次的传统做法是“意识到”编写自己的编程语言是构建文本编辑器的唯一合理方式,这样您才能最终编写最初想要编写的代码。
让我们首先简要介绍一下我的实验图形库arena-graph。要在图中具有快速边,我们希望我们的节点具有直接指向其他节点的指针。我的理解是,将*const节点强制转换为&;节点是完全安全的,只要该原始指针所指向的资源仍然存在,没有被移动,并且只要&;节点存在就不会被移动。竞技场分配给我们这些财产!我们只将*const节点存储在将与竞技场同时放置的位置-或者在节点内,或者在同一结构中的竞技场旁边。我们还确保我们分发的任何&;节点都不会超过竞技场的寿命。这保证了原始指针不会超过它们所指向的竞技场。
然而,还有一个问题。假设我们有一个如下所示的set_parent方法:
在本例中,不能保证new_parent是与self相同图中的TreeNode。如果这两个节点属于不同的领域,那么new_parent可能会在与self不同的时间被释放,self稍后会取消引用其父节点,我们就会遇到未定义的行为。
为了避免这种情况,我们需要确保new_parent和self是同一棵树的一部分,以便树的竞技场同时丢弃这两个节点。要做到这一点,一种方法是让TreeNode拥有某种唯一的竞技场ID,我们可以在任何时候将边从一个图节点添加到另一个图节点时比较这些ID。不过,这张支票令人沮丧。如果我们已经要避免时隙映射式检查,那么理想情况下我们根本就不需要任何检查,即使在添加新边时也是如此。另一种解决方案是,我们可以只让用户承诺他们不会这样做,但是将SET_PARENT标记为不安全会用无数的不安全块来扰乱我们用户的代码。
相反,如果我们可以让Rust编译器静态检查self和new_parent是否来自同一个图,会怎么样呢?这个库的赌注是你也许可以用Rust的终生系统破解它,如果你能保证:
将&;a TreeNode中的边添加到&;b TreeNode时,我们确保&a和&b具有完全相同的生命周期。
如果&;a TreeNode和&;b TreeNode来自不同的图表,则&a和&39;b的生存期将不同。
#[派生(调试)]struct NoClone(I32);fn main(){let num_1=noClone(1);let num_1_ref=&;num_1;let num_2=noClone(2);let num_2_ref=&;num_2;print_number(num_1_ref,num_2_ref);std::mem::drop(Num_2);println!(";{:?}";,num_1_ref);}fn print_number<;';a>;(num_1:&;&39;a NoClone,num_2:&;';a NoClone){println!(";{:?}{:?}";,num_1,num_2);}。
乍一看,这可能看起来很奇怪。Print_Numbers要求两个具有相同生存期的数字,但是这两个数字具有不同的生存期-在我们打印num_1_ref之前,num_2被丢弃在main中。
答案是差异。&;&39;a NoClone是';a的协变量,它具有复杂类型理论含义,但对于我们来说,这意味着您可以用大于';a的任何生存期替换';a。传递到print_number中的两个参数可以有两个不同的生存期,只要这两个生存期至少与调用print_number一样长。
#[派生(调试)]struct NoClone(I32);FN main(){let num_1=noClone(1);let mut num_1_ref=&;num_1;let num_2=noClone(2);let mut num_2_ref=&;num_2;print_number(&;mut num_1_ref,&;mut num_2_ref);std::Mem::drop(Num_2);//删除此行,使其编译println!(";{:?}";,num_1_ref);}fn print_number<;';a>;(num_1:&;mut&;';a NoClone,num_2:&;mut&;';a NoClone){println!(";{:?}";,num_1,num_2);}。
我们在这里所做的更改只是将print_number切换为接受&;mut&;a NoClone参数,而不是&;a NoClone。为什么这个是无效的?那么,我们可以在print_number中添加一行快速代码:
在我们的main函数中,这将更新num_1_ref以指向num_2。由于num_2在打印num_1_ref之前被删除,因此此交换将导致未定义的行为,因此编译器对此进行抱怨是正确的。
寿命差异如何防止第二种情况,但允许第一种情况?&;b mut T是(就像&;b T一样)协变于';b。然而,它对于T是不变的,这意味着只能且恰好可以传入T。由于本例中的T是NoClone,因此我们的新print_number要求这两个NoClone具有完全相同的生存期。
从&;a TreeNode和&;b TreeNode添加边时,我们确保&a和&b具有完全相同的生命周期。
如果&;a TreeNode和&;b TreeNode来自不同的图表,则&a和&39;b的生存期将不同。
要获得第一个属性,我们只需要确保&;';树节点对于';a是不变的。要实现这一点,我们可以使用PhantomData和包装器结构:
使用std::marker::PhantomData;#[Derate(Clone,Copy)]struct TreeNodeRef<;';a>;{Internal:&;';a TreeNode,mark_Instant:PhantomData<;&;a()>;,}Impll<;';a>;TreeNodeRef<;';a>;{fn Set_Parent(SELF,NEW_PARENT:TreeNodeRef<;';a>;){sel.parent.SET(new_parent.neras*const TreeNode);}fn Get_Parent(Self)->;TreeNodeRef<;';a>;{TreeNodeRef{Internal:unsafe{&;*self.parent.get()},mark_constant:PhantomData,}。
不过,我们仍然需要第二个属性,其中两个树总是生成具有不同生存期的TreeNodeRef。这里有很多东西是行不通的。例如,这个简化的示例最初看起来是正确的:
使用std::marker::PhantomData;#[Derate(Clone,Copy,Debug)]struct TreeNodeRef<;';a>;{//为简明起见忽略的内部节点数据mark_constant:PhantomData<;&;a()>;,}fn Same_Lifeth<;(a:TreeNodeRef<;';a>;,b:TreeNodeRef<;';a>;;';a>;){//两个节点具有相同的生存期;在此处设置递归指针}struct Tree;impl Tree{fn root<;';a>;(&;';a>;)->;TreeNodeRef<;';a>;{TreeNodeRef{mark_invant:PhantomData,}fn main(){let tree_1=Tree;let root_1=tree_1.root();{设tree_2=Tree;let root_2=tree_2.root();SAME_LIFEST(ROOT_1,ROOT_2);}println!(";{:?}";,root_1);}。
这会失败,因为root_1和root_2的生存期不同。但是将root_1的创建移到块中,我们可能会错误地编译以下代码:
Fn main(){let tree_1=Tree;{let tree_2=Tree;let root_1=tree_1.root();let root_2=tree_2.root();ame_life(root_1,root_2);}println!(";{:?}";,tree_1.root());}。
现在,根1和根2是同时创建的,因此具有相同的生存期。我们怎么才能让这变得不可能呢?最初,我们似乎可以使用闭包强制root()调用位于不同的作用域中:
Struct Tree;impl Tree{fn with_root<;a,F:FnOnce(TreeNodeRef<;';a>;)>;(&;;a self,func:f){func(TreeNodeRef{mark_Instant:PhantomData,})}}FN main(){let tree_1=Tree;let tree_2=Tree;Tree_1.with_root(|root_1|{tree_2.with_root(|root_2|{Same_Lifeat(root_1,root_2);})});}。
但是,您会发现这实际上可以编译!这怎麽可能?我的理解在这里变得有点模糊,但我非常确定,因为使用_root‘s&;&;a,Self对于&39;a是协变的,它允许构造的TreeNodeRef<;a>;也具有任意长的生命周期。我们怎样才能正确地犯这个错误呢?理想情况下,我们需要某种方式来表示传递给WITH_ROOT的FnOnce不应该具有调用者选择的生存期,而是由WITH_ROOT确定的某个唯一生存期。对我们来说幸运的是,Rust有一点魔力,叫做更高排名的特征界限,它就能做到这一点:
实施树{fn with_root<;F:for<;';any>;FnOnce(TreeNodeRef<;';any>;)>;(&;self,func:f){func(TreeNodeRef{mark_Instant:PhantomData,})}}。
使用上面的代码,我们的main将正确地无法编译,因为将保证root_1和root_2具有不同的生存期。
虽然这具有不需要不安全的优点,但不幸的是,这意味着您的图结构有一个生命周期:
我发现这样的使用寿命会带来很多障碍,并且会导致界面不符合人体工程学。这还意味着Graph在插入节点后永远不能移动,这是一个不必要的要求,因为Arena分配的节点总是在堆上。
有时候我们做事情不是因为它们是好的。我们这样做是因为它们不好。
就在这篇文章发表几分钟后,我看到了亚历克西斯·贝因斯纳(Alexis Beingessner)的论文,它在第6.3节描述了这项技术,尽管幻影数据<;Cell<;>;是用来使生命周期不变的,而不是我的幻影数据&;&;是一种mut&;a()>;。引用亚历克西斯的话: