https://course.rs/advance/circle-self-ref/self-referential.html
Rust中更易用的自引用类型(下)
好的,我会按照您的要求整理这篇内容,使其变得合理通顺,同时不遗漏任何内容。以下是整理后的内容:
Rust中更易用的自引用类型(下)
我们继续来看这篇文章,标题是”更易用的自引用”。讲到不可移动的类型,我们之前也讲到了这个不可移动。它具体是什么样子的呢?
我一直在要求大家假装相信,我们可以原地构造一个ping,可变引用self的这个类型,把这整个self把它可变引用,把它聘住,并且一切都会像我们期望的那样工作。其实我自己也不确定,但是我的这篇文章的叙述,让我们假装可以做到这一点会更容易理解。
当然了,真正的解决方案是什么呢?是完全摆脱ping,取而代之的是类型本身应该能够传达它们是否具有稳定的内存位置。也就是说用类型系统去做到这一点,不是说我自己人为地去告诉编译器怎么怎么做,这个东西不能动,聘住它。我告诉编译器这样做,而是让类型本身把这个内存位置稳定下来。
最简单的实现方式,是添加一个新的内置自动特性。就有点像这个自动特性安ping一样的,安ping的它也是自动特性。但凡是默认都是安ping的。它有一个内置的自动特性,相当于就是编译器自己给它加上了。对于开发者来讲,它是透明的。也就是说它告诉编译器类型是否可以移动。默认就是可以移动的,那alter treat move也相当于会加一个move这个特性上来。这个特征上来就是正常的话,大家都是可以动的,都可以动,自动的,可以动。
这当然不是一个新的想法了。至少2017年起,我们就知道木特性的一个可能性。那是我开始从事Rust开发之前,Rust社区中有一些人强烈支持目,但最终我们并没有采用这种设计。我认为现在回想起来,大多数人都会承认ping的缺点确实存在,因此重新审视目并克服它的局限性,似乎是一个好主意。
木特性将是一个语言级别的特性,它控制以下功能的访问:
- 可以按值传递给函数和类型
- 可以通过可变引用传递给memory swap, Memory Take, memory replace函数
- 可以用于任何与之前几点语法等价的用法,比如说赋值给可变引用,闭包捕获等
当然了,我们来看这个木,它是指可以移动。所以其实它很容易跟我们所讲的这个所有权,这个移动容易有点混淆。所以说从我来讲,我来讲就说这个move的话,真正要这个特征的话,可能就不应该叫move,可以叫一个其他的什么东西,移动的这个名称也好。因为这样就可以避免这种混淆。这个所有权的移动和这个内存位置的移动,是两码事。所以这个特性可能应该起一个其他的名字,会更好一点。
相反,当类型实现了不可移动的时候,它们将没有办法获得任何这些功能。这上面这些功能都没有了。这就意味着一旦它们拥有固定的内存位置,就没有办法再动它们了,就没法移动它们了,在内存中的位置固定了。
默认情况下,我们会在所有的上下文中假设类型都是move的,就是你在内存中都是可以动的。除非明确地使用了什么呢,使用了这个加问号木就是你能不能动,不知道。也就是说放弃了这个默认的木,opt out了。因为正常来讲它是自动特性。
以下是构成移动的例子:
- 能够在内存中动,像这个swap。mute x new thing, Mute y new thing,然后swap一下,那你内存当中动了。
- 按值传递也是动了,也是内存中会动了。
- 返回一个值也是。
- 用到这个move闭包的时候,木闭包的时候也会。闭包当中也会用到这个。
以下是一些不构成移动的例子:
- 传递一个引用,那引用的时候你不需要内存中不要动。不需要动的话,你引用这个地方就行了。
- 还有一种是这个可变引用也是OK的,但是你必须要注意你去怎么去用它。这个地方就是你引用的地方不要动,或者是可变引用也好,你都是在那个位置。
按值传递类型的将永远不会跟不可移动的类型兼容,因为这正是移动的定义。移动的定义就是这个,就是你是按值传递的,你拷到那个地方去了,你那肯定内存中肯定会变动了。
按引用传递类型将始终跟不可移动类型兼容的。你引用你那个内存位置要变干嘛,我要引用它,指过去。因为它是不可变的。
唯一存在歧义的地方就是当我们使用可变引用的时候。因为像这个memory swap这样的函数允许我们违反这个不可移动的保证。如果一个函数想要获取一个可能不可移动的可变引用,就需要添加这个问号木。如果函数没有在可变引用上使用加上这个问号木的话,那么就不能给它传递不可移动的类型。
那实际操作起来就是这样的:
- 如果是按值传递的话,就不能够把不可移动传递过去
- 如果是按引用传递的话,就可以不可移动
- 按可变引用的话,就不能够把不可移动传递过去
- 还有一种情况就是加上这个问号,就是能不能动还不知道,这种的话就可以
默认情况下所有的这个cat可变,用T约束都隐含了是mod,因为自动特征。只有在明确选择加上问号move的情况下,才能够传递不可移动类型。实际上大多数地方都乐于添加是否能够移动,因为使用可变引用更新字段,比使用memory swap整体替换引用要常见得多。
内部可变性在这种规则下可能也没有什么太大问题,因为是内部可变。因为即使访问通过共享引用进行更新,指针中的值也必须遵守我们之前制定的规则。这些规则默认情况下是安全的。为了安全准确,我们还需要考虑内部可变性。内部可变性其实没什么关系,你外面已经有东西裹着了,你里面变就里面变。它允许我们通过共享引用来修改值,但只能在runtime时候有条件地将其转换为可变引用。
仅仅因为我们允许在runtime把引用T转化为可变引用T,并不意味着我们应用于系统的规则就不再起作用了。假设我们在MUTEX内部,像这个内部可变性,像MUTEX就是这样的一个东西,因为MUTEX本身是个wrapper。它内部持有一个可变引用T不可移动,如果我们尝试调用derive mutable方法,就会得到一个编译错误。因为这个约束还没有声明这个T问号mod,我们或许可以添加它,但由于它不是默认的行为,我们可以在添加之前有机会去验证它的健全性。
总之,关于这种方法应该如何工作的理论先讲到这里。我们尝试更新我们之前的例子,用这个不可移动替换这个ping。这里面应该只需要在gpath future上面添加一个不可移动就行了。在这里把里面给它添加一个不可移动就行了。一旦实现了这一点,我们就可以把这个构造函数改成返回super cf,而不是super ping。引用tick super mutable self,我们已经知道使用类似于SUPERC的东西向固定内存位置写入内容的案例似乎是可以的。我们只需要添加一个自动特性,告诉类型系统不允许移动操作。添加一个自动特性不可移动,各种类型系统的话就知道了。
我可能应该早点说,但我现在再说一遍,就是在这篇文章中,我故意不去考虑向后兼容性。再次强调,重点是把不可移动类型这个复杂的设计空间给它分解成一个个我们可以足够解决的小问题,就是化整为零地去解决问题。一点点地去解决,break down一下。那怎么去桥接或者是对接,或者是说这个过渡一下,ping和不可移动是我们迟早要解决的问题,但现在还不是时候。
对于a think和future来说,这应该是可以工作的。这允许我们自由地移动,转换为into future的一步。只有当我们准备轮询它们的时候,才会调用into future来获取一个invitation future加不可移动步。这样的系统跟现有的pin系统相当,但是不需要在签名中使用ping了。
为了方便起见,下面是如何使用这种方式重写future的签名。在future的签名当中就会需要self pin,然后pull self只是它的签名。但是如果用这个木的话,那就用这个就行了。可变引用self,然后就可以了,它的签名就可以不需要在里面含有ping了。
这也意味着这不再需要ping投影,不再跟drop不兼容了。因为这是一个控制语言行为的自动特性,只要基本规则健全,它跟Rust的其他所有部分的交互都将是健全的。另外对我来说,可能最重要的是这将使我们能够使用方法和函数来编写future状态机,而不是像目前只能把所有内容都塞进power函数体中。在过去6年里手动编写了大量的future代码之后,我简直没法形容我有多么渴望能够做到这一点。
我们再看改进后的启发性的例子。就是刚开始的那个例子,现在我们已经涵盖了自引用的生命周期,原地构造,理解了i think应该返回into future,并且看到了不可移动。我们就可以把这些特性结合起来,重新设计我们这个例子。这是我们最常使用常规的single wait代码编写的例子。这是常规的:
1 | Net data the name a pack, can name a wait |
下面是使用这些新功能后,a single function gpath将会变成什么样:
要注意tick self生命周期future的不可移动实现,所有地方都没有ping以及类型的原地构造。像这个into gpts, into for into, Gu pets future,这里面你可以看到就已经完全不需要ping了,这就是没有。你看这里就不需要ping了,而且这是签名当中不需要了。
最后我们可以把这个gpts await调用转换为我们构造并完成了具体的类型。这里面就可以变成这样的就行了:不可移动,不需要这个ping值在里面了,into future。
通过这种方式,我们应该可以得到一个能用a sync代码块的例子。它是被去糖为具体的类型,而不需要任何地方生ping的特性了。访问其中的字段不会经过任何类型的ping投影,也不再需要这个P然后去投影等操作。也就是说,我们要拿到这个结构体里面的这个字段的时候,就不需要利用P然后去投影它,我们直接就去用它就行了。这个不可移动性只是类型本身的属性,也就是说它的操控是谁呢?是类型系统去干这个事。当我们在需要使用它们的地方构造它们的时候,就已经具备了。构造完了类型系统就接管了,不需要我们再去做其他的事了。
顺便提一下,使用这些特性的函数总是希望使用t into future,而不是t future。要注意,这不是一个很大的改变,实际上我们现在就应该这样做了。但是我还是提一下,就是要防止人们对这个并发操作的边界感到比较困惑。
我们刚刚讲了不需要在这个ping里面整个的ping里面去投影。那怎么做到这一点呢?它就是分阶段的初始化。因为分阶段的初始化,你就能够去直接来触达这个字段。
我们在例子中没有展示这一点,但是自引用类型还有一个值得关注的方面是什么呢?阶段性初始化。也就是说分阶段去初始化。这指的是在不同的时间点初始化类型的一部分。类型里面的有一部分来初始化,其中的一个字段先初始化,再初始化另一个字段,就分阶段。
在我们的例子中,我们不必去使用它,因为自引用它是在这个option里面的。在option里面的,这就意味着当我们初始化类型的时候,我们可
好的,我会继续整理内容:
我们可以简单地传递None,一切都正常了。但是如果我们确实想初始化一个自引用,我们该怎么进行呢?像这样的一个自引用,这个name引用data的这种怎么用呢?
当然由于这个string是堆分配的,我们这里面的string,这个data是string是堆分配的,它的地址实际上是稳定的,就在堆上面。因此我们可以写成这样,就name data splay等等,其实这样就行了。
这显然是在作弊,并不是我们想要人们做的,但它确实指出了解决方案可能应该怎么样运作。我们首先需要一个稳定的地址来指过去。一旦我们有了个地址,我们就可以引用它了。如果我们必须一次性构建整个对象,我们就没有办法做到这一点。但是如果我们可以分多个阶段来进行呢?
那正是Nico最近关于阶段性检查和view类型的帖子所讨论的内容。我们将这将允许我们把这个例子改成这样:
1 | string tick self string |
这里面执行不可移动,然后这个net cat data可以看到这里,那初始化的时候先初始化data。这是先初始化一部分分阶段,然后再初始化name。完成了这个初始化整个的初始化。这个自引用是分两步来完成的。
我们首先初始化cat中的own data,这个own的data。一旦有了它,我们就可以初始化对它的引用了。先初始化它,再初始化它。这些引用将是tick self的,我们加入了一个super net注释,来表示我们将它放置在调用者的作用域里面。之后的一切都没有问题了。
我们从pin迁移到move。当然我们已经提出了这个move的这种概念了。那么move特性已经,特征我们都讲好了。那怎么从ping上面迁过去呢?你讲的概念不错,但怎么实际运用上去呢,是个问题。
这篇博文并没有涵盖从现在基于ping的API到新的基于mod系统的这个迁移过程。我们想要放弃pin而使用mo,我认为唯一可行的方法是创建新的特征。那么这些特征在签名中不包含ping了,并提供从旧的特性到新特性的过渡的实现。
一个基本的方法,可以这样用使用显式的方法进行转换。我们写一个rapper,当然也可以用blanket implementation。如果不理解什么叫躺实现,可以看一下我之前的这个视频,专门讲了一个视频,讲到这个毯子实现。所以这个毯子实现其实就是它的影响面非常广,整个编译器里面都会有它。那么就相当于把一个东西替换成另一个东西,跟所有人广播一样的。你要做这个事。因为它是blanket的implement。
所以但这地方也可以用显示方式转换,比如这里面做了一个wrapper。wrapper就是把老的这种future,然后转换成新的future。中间其实就做一个对接,接过去桥接一下过去。就像一个代理一样转换过去一样的。转过去,你一旦到我这里,我就转给它了。中间一个中间人转过去了一样,类似这种桥接的方式接过去,过渡一下。
我已经重复这句话至少3年了,但是如果我们想要修复ping的问题的话,第一步需要做的是不要让问题变得更糟。如果标准库需要修复一次future特性,这就很糟糕,但没关系,我们会找到解决方法。但是,如果我们把ping绑定到许多其他的特性上面了,问题就变得复杂了。因为一改就动的太多了。
我也不确定我们能否摆脱ping,这是一个问题。因为ping被广泛的不喜欢,我们积极地想要摆脱它与自引用类型的兼容性。不仅跟迭代相关,它还是一个通用属性。也就是说ping影响最多的是谁呀?影响最多的是自引用。就自引用类型的兼容性不仅跟迭代相关,因为那为什么ping很难摆脱的原因,它其实其实讲的其实就这个概念。就是ping为什么难摆脱呢?是因为自引用要用到ping,那自引用又应用的很广泛,跟各个东西都在交互。所以说如果要摆脱ping的话,就要解决自引用上面的问题。
所以它还是一个通用属性,最终会跟几乎所有的特性、函数和语言特性进行交互。这是慎用,慎用用的太多了。那move可以跟任何其他特性组合,因此不需要特殊的这个pinder read或者任何东西。相反,一个类型只需要实现read加move,就足以使自引用读取器工作了。那我们就可以对任何其他的特性组合重复,这样去做就行了,都去这么做就行了。
原地构造当然会改变这个特性的签名,但是为了以相互兼容的方式来支持它,我们所需要做的就是使特性能够选择可以执行原地构造。而能够逐步推出这样的功能,正是我们致力于这个效应方型化的原因。也就是说效应系统我们之前的视频讲过,就效应系统,就是其实可以看到我们逐步的在这里面也用到这个效应系统当中的东西了。它能够影响到谁,你看这里面就maybe super,这里面就是相当于是效应系统中所发挥的作用。就是它会不会要用到这个,会不会受到影响。
如果我们希望自引用类型普遍有用,那么我们实际上需要我们拥有大多数其他的特性进行组合。所以真正到达那里的第一步,是停止在标准库中稳定任何使用ping在它签名中的一个新特性。所以新的这种尽量不要去用ping了。
使不可移动类型变为可移动。到目前为止,我们已经讨论了很多关于自引用类型,以及需要确保它们不可移动的原因,因为移动它们会带来负面影响。但是如果我们确实允许它们移动呢?在C++中,我们用称为移动构造函数的功能实现这一点。如果我们在Rust中支持自引用类型,那么支持它似乎也不是一个很大的进步。也不是什么不得了的东西。其实我们在之前的视频中也讲到过这种方式,就是移动构造。
在我们继续之前,我想先做个说明。我听过一些使用C++中的移动构造函数的人说,它们使用起来可能很棘手。我并没有使用过它们,所以我没法根据这个经验来发言。就我个人而言的话,我并没有任何觉得需要移动构造函数的用例,因此我对知识它们并不赞成或者反对。我写这部分的内容,主要是出于学术的兴趣,因为我知道会有人对此感到好奇,而这种机制应该怎么样运作的。
规则似乎也相当简单。Nick Masaki最近写了一篇关于clam的文章,推荐两部分文章其实我们之前翻译过了。就提出了一个新的这个克隆特性,来填补克隆和copy之间的空白。其实也不是空白,它是把这个克隆的一部分把它切出来。不是空白,其实。那这个特性适用于这个比较容易克隆的这个类型,比如说arc、RC类型的,并且使用这个auto clean编译器会自动插入需要的claim。
所以它这里面其实它把这个比如说当这个木闭包获取一个实现的claim类型,但它已经在别处用的时候,它会自动调用claim来并这个通过。所以它就想到跟这个auto clean比较类似,有个auto move。是这么想的。
好的,我会继续整理剩下的内容:
所以使不可移动类型能够重新定位的工作方式跟自动声明非常相似,就是auto claim很相似。我们需要引入一个新的特性,这里面我们把它称为relocate,它有一个relocate的方法。每当我们尝试移动一个原本不能动的值的时候,我们都会自动调用relocate来代替。
那relocate特性的签名将采用可变引用作为这个cf,并且返回一个原地构造的cf实例。这种方式去做。请注意这里面的self签名,我们通过可变引用来获取它,不是拥有所有权,也不是共享。这是因为我们在编写的是不可移动类型的into等价物,但是我们不能通过值来获取self,所以我们必须通过引用来获取它,并告诉人们使用memory swap替换它。
因为我们之前用到的这个cat的例子,我们就可以像这样去实现它,就super net cat。这里面相当于是构造一个新实例来去做到这一点。那这里面就相当于是原地构造。
我们这里我们做了一个不太严谨的假设,我们需要能够从这个cf获取已经拥有所有权的这个数据,而不会因为已经owned了这种data而遇到数据没法移动的问题,因为这个数据已经被cf借用了。这是一个我们需要解决的普遍问题。我们可以通过一种方式来解决这个问题,比如说在主结构里面创建一个引用针,来确保类型始终有效。就相当于把它压制针,把它借用了,你不能乱动了。但是我们会使直线类型失效。所以在这个例子,cat将会实现move,即使它拥有了tick cf生命周期,因为我们可以自由移动。
当一个类型在传递给relocate之后被释放时,它不应该调用它的drop实现,因为从语义上来说,我们并不是试图释放这个类型,我们所做的只是更新它在内存中的位置。
根据这些规则,对结构体中的TCCF访问,将在self f不可移动步和self relocate的时候可用。
我再次强调,我并不是主张直接在Rust中引入这个移动构造函数,我个人对它们持比较中立的态度。我可以被说服支持或者反对都行。我主要想做的就是,在移动构造函数可能发挥作用的情况下,至少演练一次。因为应该知道可能知道什么,有可能存在渐进的这种路径。希望这一点能够很好地传达。
扩展阅读:
之前看的一些文章,像这个ping的RFC。本身就是提出ping的这个文章那就值得看的,因为它描述了我们最终采用的不可移动类型的系统。特别是它对比了ping和BO以及关于缺点的部分,值得再次回顾。尤其是在我们把它跟ping文档进行比较,并查看NFC中没有出现,但后来变成主要的设计问题的内容。比如说这个P投影,跟语言的其他部分交互的时候带来的影响等等。这个ping出现了各种问题,但是一个临时解决方案。
TMANDI提供了一系列关于异步内部机制的有趣的文章。具体来说,他介绍了ASQU是怎么去糖化成基于future的状态机的。这篇文章正是以这种去糖化作为启发性例子,因此对于这些热衷于了解幕后发生的读者来讲,这是一个非常棒的资源。
Rust的参考书中的关于await去糖化的部分也值得一看,因为它捕捉了编译器中的现状。
最近像这个马格尔一样的演讲和支持C++移动构造函数的create也值得读。他提出了的就是new特性,是我还没有完全理解的,但很有意思。其实他这个new特性就是原地构造。应该是这个意思。但他很有意思,它可以用这个原地构造栈和堆上面的类型。理想情况下类似于这个SUPERLIGHT或者super type的技法,也可以知这一点。
你可以把C++的移动构造函数视为不可移动类型的进步演化,因此存在许多共享的概念也就不足为奇了。
两年前,我尝试过制定一种利用view类型进行安全ping投影的方法,但这几个方面遇到难题。尤其是安秀,就是我不确定怎么样处理这个represent packet的交互,如何使用这个job兼容,以及怎么样把这个安ping标记为unsafe。对于后一点可能存在途径,但我不知道针对前两个问题有任何实用解决方案。这篇文章基本上是那篇文章的一个续篇,但是把前提改成这个替换ping了。
那立刻关于view类型的系列文章也值得读。他的第一篇文章讨论了view类型是什么,它们怎么工作,以及它们为什么有用。在他最近的一篇文章中,他讨论了view类型怎么样融入一个更广泛的借用检查器部分的路线图。在他的最后一篇文章中,他直接介绍了使用view类型进行阶段性初始化,这也是我们这篇文章中讨论的跟自引用类型相关的特性之一。
最后我建议看一下Bowers create,它是通过利用红和闭包在稳定的抓住实现了对自引用类型的安全的阶段性的初始化。其实我感觉这个对自引用的阶段性初始化这个东西还是很有意思的,就是在操控上面就摆脱了ping的投影,这个是比较好的一个东西。他的工作方式是首先初始化使用owned的data的字段,然后执行闭包来初始化引用数据的字段。就引用的这个字段再初始化,它就分步初始化。本文所说的利用view类型的阶段性初始化模拟这种方法,但是可以直接通过语言本身,通过一种普遍有用的特性来去实现它。
总结:
在这篇文章中,我们用自引用类型分解成四个组成部分:
- unsafe和tick cf的生命周期,这将使表达自引用生命周期成为一种可能。
- 类似于在SUPERLIGHT或super tab的等价物,以安全地支持外部指针。
- 一种相互兼容的添加可选的super tab语法的方案。
- 一个新的move自动特性,它控制对移动操作的访问。
- view类型特征,它将使构建自引用类型成为一种可能,就不需要通过option方式了。
这篇文章提供了最终的见解,是今天的ping加上安P系统可以通过创建可以返回不可移动类型的move这个wrapper来模拟move在异步上下文中的模式。将是构造一个implementation into future加mood一个wrapper。它通过外部指针原地构造一个implementation future加不可移动的future。
人们普遍都不喜欢ping。据我所知,人们普遍支持探索替代解决方案,比如木。目前唯一在标准库中使用ping的特性是future。为了促进从pin迁移到类似move的特性,我们最好不要在标准库中进一步引入任何基于ping的API了。从单个API迁移需要付出努力,但最终似乎是能行的。从大量的API中迁移,那就需要更多的精力了,并且使我们永远陷入了ping的困境的可能性就更大了,因为一用到ping的麻烦事情就多了,复杂的事情就太多了。
这篇文章的目的是将不可移动类型这个庞大而令人恐惧的问题分解成它的组成部分,break down一下,以便我们可以逐个击破。文章中的语法和语义都不是具体的或者是最终的。我主要想做的就是至少演练一遍能够使不可移动类型工作的所需要的一切,这样的话其他人就可以深入挖掘、共同思考,我们就可以开始细化具体的细节了。
好,这是这篇文章的所有内容,很有意思。
进一步简化Rust自引用类型
非常抱歉之前的回答有所省略。我现在会整理完整的内容,不遗漏任何细节:
这篇文章标题为《进一步简化Rust自引用类型》,发表于7月8日,作者是RUSSIA555次。这是一篇续写文章,作者在7月1日写了一篇相关的文章。
文章讨论了如何引入更合理的自引用类型方法,主要通过引入以下特性来实现:
- 某种形式的tick on thief和tick self生命周期
- 一种安全的Rust外部指针(称为super net或super type)
- 一种不破坏相互兼容性的引入外部指针的方法
- 一个新的move,自动标记类型为不可移动的特性
- 视图类型(view type),可以安全地初始化自引用类型
作者提到上一篇博文得到了很多好评,后续讨论也很有意思,他学到了很多可以用来进一步改进设计的东西。
作者指出,并非所有的自引用都是不可移动的。如果引用的数据是堆分配的,那么这个类型实际上是可以移动的。在编写协议解析器时,通常会先把数据读入堆分配的内存中,这表明许多自引用类型可能根本不需要不可移动或任何pin概念就能正常工作。
如果只想支持堆分配类型的自引用,那么只需要一种初始化它们的方法(view类型的方法)和一种描述自身生命周期的能力(tick and safe)作为最低要求。
关于生命周期,MATTM指出tick self可能不够用,因为它指向整个结构体,可能会变得过于模糊而不够实用。相反,我们需要能够指向单个字段来描述生命周期。作者建议使用基于位置的生命周期,让引用始终具有隐式的、唯一的生命周期名称。例如,将”tick self”改为”self.first”这样的基于路径的形式。
关于自引用的稳定性,作者指出对于把数据存储在堆上的资源类型,实际上不需要强制标为不可移动。Dove of Hope指出,如果编译器已经知道我们指向的是结构体内部的某个字段,那么当我们尝试移动结构体时,编译器就能够确保更新指针。这几乎可以完全消除对不可移动的需求。
然而,对于原始指针(裸指针)操作,编译器无法提供相同的保证。为解决这个问题,作者提出引入一个新的自动标记特性Transfer,作为Rust的destructor系统的补充。所有带有tick safe生命周期的类型都自动实现Transfer,而只有包含+transfer的限制才能接受impl Transfer的类型。
作者重新审视了relocate特性,并给出了一个基于async/await的示例代码。使用基于路径的生命周期和编译器自动保持引用稳定性的方法,可以简化代码,无需使用pin或原地构造。这只需要编译器在移动值的时候更新自引用指针的地址,会稍微增加一些代码生成量,但应该能带来良好的性能。
尽管如此,作者认为不可移动类型仍然有其价值,特别是在以下情况:
- 与外部函数接口(FFI)协作时,FFI本身就要求数据不可移动
- 某些追求高性能的数据结构,在栈上使用大量自引用数据,更新这些引用数据的代价非常高
- 实现侵入式链表等特殊数据结构,如Rust内核中使用的不可移动类型
作者讨论了async函数的返回值类型问题,认为implement Into Future可能更合适。他提到在2024版本中,正在将范围语法(0..12)的返回值类型从IterError改为IntoIterator,这与Swift的行为保持一致。
最后,作者总结道,实现一套完整的自引用类型功能需要相当多的依赖项,但可以逐个特性进行实现。他给出了一个包含所有特性及其依赖关系的图表。tick and safe特性看起来并不遥远,基于位置的生命周期和view类型也受到了一些积极关注。
作者对这些特性的实现持乐观态度,认为可能在2027年的版本中就能看到这些改进。他指出,pin是一个临时的解决方案,如果编译器能够在值移动时自动更新指针,那么pin的重要性将大大降低,可能会被其他机制所取代或大幅削减其应用范围。
原文链接: https://dashen.tech/2018/07/25/Rust自引用类型/
版权声明: 转载请注明出处.