会记住自己指向何处的引用!2026 年 10 月 11 日——瓦伦(Valen)有了新的灵活借用检查器。我热衷探索新内存安全方法并以奇怪方式融合,这是我追寻内存安全圣杯的一部分:它要像 Rust(Rust)的借用检查一样强大,要像引用计数或垃圾回收一样简单灵活。我一直认为得解决三个挑战:更宽松的借用检查器、读取更复杂数据,以及让两套系统同时工作。
正文
能记住自己指向哪里的引用!
这篇帖子介绍 Valen 新的灵活借用检查器。欢迎!
我一直在寻找内存安全的圣杯:既像 Rust 的借用检查那样强大,又像引用计数或垃圾回收那样简单灵活。我早就怀疑,要找到圣杯,必须解决三个内存安全挑战:
- 能不能做一个更宽松的借用检查器?也就是让一个借用检查器支持对同一个对象的多个读写借用引用。
- 能不能让借用检查器读取更复杂的数据?这里指的是像 C++、Java 那样对象可以任意互相引用的完整数据图,而不是 Rust 那种通常不能让结构体长期互相引用的结构。
- 能不能让这两套系统同时工作?
这听起来模糊又晦涩,下面会解释它们的含义,以及 Valen 可能如何解决。这篇帖子主要关注第一块:Valen 更宽松的借用检查器。我会讲它是什么、能做什么,以及怎么工作。
为了表述清晰,这篇帖子用了一些语法糖:自动解引用、直接索引结构体(如 my_vec[0])、从 group 推断类型(如 entity in world.entities[]),这些会在 11 月到来。除了这些语法调整,借用检查器对这里讲的所有内容都适用。
顺带我也会解释一些更奇怪的点,比如:Valen 的借用检查器如何塑形,以便让 Valen 代码调用 Rust 代码;编译器如何记住引用指向哪里;以及 Valen 只有一种借用引用,而其他借用检查器有两种。这篇帖子篇幅很长,我会在适当时候提示可以跳过。我们开始吧!
分组借用:一次险些夭折,却潜力巨大
Valen 的方法建立在 Nick Smith 设计的“分组借用”提案上。它的核心概念是:编译器记住引用指向哪里,并用这个信息检查程序是否内存安全。
这个设计的命运一开始很坎坷,几乎被时间埋没。它最初是为 Mojo 设计的,但我没能说服决策层采用。分组借用在有机会成功前就夭折了。但我知道它有巨大潜力,只要能 somehow 进入世界。于是我们花了几个月打磨解释、理清好处,并在去年写了一篇文章,希望其他语言采纳。
结果它传播得很广:
- Google 的 Carbon 语言完全转向分组借用,还致谢了我们的文章。
- Ante 开始把它融入语言,见“Shared Mutability and Stability”。
- Plecra 的 exclusion typing 用分组借用和 GhostCell 的概念重新设计 Rust 借用检查器。
- Zeta 把分组借用与 disjointness 结合,把运行时事实纳入编译期内存安全;它能证明两个引用指向同一份数据的不同部分,例如在知道 i != j 时证明 list.getmut(i) 和 list.getmut(j) 是不相交的。
- Fang 也在设计中采用它,见“Cyclic References in Fang”。
- 现在我的 Valen 把分组借用与 Vale 的方法结合起来,还能与 Rust 代码互操作。
这些语言大多在攻克同一个挑战:如何做出更灵活的借用检查器。比如 Valen 想让借用检查器灵活到能配合引用计数和分代引用,Carbon 想让它灵活到能配合 C++ 代码。
这很难,因为借用检查和其他方法之间有根本冲突,可以概括为:
- 借用检查有“共享-xor-可变”限制:如果你持有对象的引用,别人就不能改这个对象。
- 其他方法希望允许你持有对象引用的同时,让别人改它。
C++ 想要第 2 点,因为那是我们常见用法:一个 unique_ptr 拥有对象,一堆非临时的原始指针指向它。引用计数和垃圾回收也想要同样的东西。你可能觉得“这肯定没法调和”,看起来确实如此。但分组借用真的解决了这个冲突:它把“共享-xor-可变”放宽成“没有 use-after-free”。这句话现在很晦涩,等后面解释分组借用原理时就明白了。下面先看分组借用能做什么,再讲它怎么工作。
分组借用能做什么
分组借用能在没有运行时开销的情况下给程序内存安全。它在编译期捕获 use-after-free 问题,而且限制比以往方法更少。
下面是一个编译期捕获 use-after-free 的例子:
struct Entity {
hp int;
}
struct World {
entities Vec
world.entities.push(Entity(42)); let first_ref = &world.entities[0];
world.entities = Vec
// Compile error: Used a borrow after invalidated return first_ref.hp; }
这是一种异常灵活的做法。通常编译器会难以处理下面这个程序,但分组借用理解它是安全的:
func main() int {
let list = Vec
// Make two refs to the list let refa = &list; let refb = &list;
// Mutate the list through both refs refa.push(42); refb.push(73);
return 42; }
分组借用接受很多通常让借用检查器头疼的模式,例如:
- 多个局部变量指向同一个对象,并且可以通过其中任何一个写入(如上)。
- 一个参数指向某个对象,另一个参数指向该对象内部的某处,只通过后者写入(下节会看到)。
- 多个函数参数指向同一个对象,并且可以通过其中任何一个写入。
- 做一个“回滚”结构体,在它离开作用域时修改对象,类似 ScopeGuard 模式。
- 多个结构体持有对同一个子系统的可变引用,比如一堆 File 对象各自持有对同一个 FileSystem 对象的引用。
- 还有更多,下面会讲。
这些是我们喜欢 C++ 的模式,但没有语言能在编译期以内存安全的方式做到全部这些。
一个更有趣的例子
例如,下面是一个 step 函数,它接收一个 World 引用,以及一个指向其中某个 entity 的引用。重点是 entity in world.entities[] mut,下面会解释。
struct World {
entities Vec
注意这几个部分:
- in 表示“指向……之一的引用”。
- world.entities[] 表示“world 的 entities 的元素”。
- mut 表示“可以修改它”。
合起来:entity 是一个指向 world 的 entities 中某个元素的引用,而且我们可以修改它。
这就是核心概念:编译器知道引用指向哪里,因为用户通过 in 指定了它指向哪里。
下面看看其他语言里的等价写法,你就明白这有多好。首先是 C++ 的等价版本:
struct World {
vector
当然,C++ 版本需要小心才能维持内存安全,因为 C++ 不在编译期检查内存安全。
下面是 Rust 的等价版本,它确实有自动内存安全:
struct World {
entities: Vec
Rust 一般很好用,但在这种场景下不如 C++ 快或简单:每当需要在同一层级里拿 &mut 到别的东西时,都得重新取引用,而且这里有多次边界检查,可能 panic。在 Rust 里,为了调用 advance 需要 &mut Entity,然后为了把 &World 交给 getcollisionfor_entity,又得扔掉它,之后再重新取 &mut Entity 来调用 resolve。
另一个办法是把程序重构得更“扁平”,这各有取舍:更容易配合借用检查器,如果游戏做 ECS 式迭代也会更快,但会失去单所有权的 RAII 好处。比如把异构树拆成多个数组后,父节点删除时子节点不会自动删除。而且如果拆分太多并行集合,可能会有“ID 追踪”的缓存缺失变慢。我警惕任何把人引向某种架构的内存安全方案,因为那可能不是最好的。即使是分组借用也在某种程度上这样,所以我很期待融入引用计数或分代引用。
如果想更 fancy,可以用 GhostCell,给数据加上 brand,再用 GhostToken 访问:
struct World<'brand> {
entities: Vec
你可以看到这个困境:
- C++ 例子简单,但不内存安全。
- 普通 Rust 内存安全,但稍慢,也稍复杂。
- 加 GhostCell 后能达到 C++ 的速度,但仍然有点复杂。
而 Valen 的做法能得到又快、又安全、又简单的代码:
struct World {
entities Vec
这是相当漂亮的简化!注意,这里的 mut 不像 Rust 的独占引用 &mut,同一个 entity 还可以有其他引用。而且这个函数不能修改整个 world,只能修改 world.entities[] 里面的东西;这是个很大的超能力,意味着优化器能更好地优化这个函数及其调用者。entity in world.entities[] 实际上指向“entities 数组里那组对象中的某个地方”,是一个粗略位置。现在终于要解释它怎么工作了。
Valen 的借用检查器如何工作
简单说,这个方案由五个机制组成:
- 每个对象都由一条路径描述,遵循单所有权。
- 每个引用都记住它所指向对象的路径。
- 销毁对象时,所有指向该对象路径的引用都不再可用。
- 函数签名描述它会修改哪些路径。
- 调用函数时,所有指向被修改路径内部的引用都不再可用。
这很抽象,下面逐一解释。
单所有权
Valen 的借用检查器基于单所有权,和 C++、Rust 一样。每个值都由包含它的对象、数组、栈帧或全局“拥有”。(后面我们会用引用计数放宽这一点。)如果你已经了解 C++ 或 Rust,可以跳过;否则下面解释单所有权是什么。
在单所有权里,每份数据都有一个“所有者”。例如,下面这个 C++ 程序:
include
struct Engine { int fuel; };
struct Ship { unique_ptr
或者这个 Valen 程序:
struct Engine { fuel int; };
struct Ship { engine Box
我们可以这样说:
- main 的栈帧拥有 vector
ships。 - vector
ships 拥有每个 Ship。 - 每个 Ship 通过 unique_ptr 拥有它的 Engine。
- 每个 Engine 拥有它的 int fuel。
- foo 不拥有这个 vector,它只是有个原始指针。
- main 是唯一没有所有者的东西。
如果你写过 C,可能也会这样思考:追踪一个对象从 malloc() 到 free() 的旅程,所有指针经过的变量/字段都在处理“拥有指针”,就像侦探追踪证据的监管链,也就是某一时刻谁“负责”它,或者说谁负责释放它、或负责把它交给最终会确保它被释放的人。甚至 Java 和 C# 程序员有时也按单所有权思考:如果你要在某个时刻调用对象的 dispose/cleanup/destroy/unregister 等方法,你可以追踪它从 new 到这个(概念上破坏性)方法调用的旅程,那些变量/字段就是在处理它的“拥有引用”。
单所有权是很多语言的基础:
- 加上不受限制的指针和引用,就是 C++。
- 加上分代引用和区域借用,就是 Vale。
- 加上独占性检查引用,就是 Rust。
- 加上分组借用和另外几个机制,就是 Valen。
在单所有权下,每个对象都可以用一条“路径”描述,下面讲。
路径
每个对象都可以用一条路径描述。例如:
struct Entity { hp int; }
struct World {
diameter int;
entities Vec
... }
这个程序里的所有路径是:
- x
- world
- world.diameter
- world.entities
- world.entities[]
- world.entities[].hp
路径总是以以下之一开头:
- 局部变量,如 world。
- 函数参数。
- 函数的“group 参数”,后面会看到。
之后可以接:
- .diameter 命名成员。
- [] 命名集合的元素。
- .SomeType 命名 union 的变体,比如 my_engine.WarpEngine。
- 以及其他几种后面会讲的形式,比如关联 group、多类型 group 的子类型等。
因为单所有权,每份数据只有一个所有者,所以每个对象只有一条路径。严格来说,如果用关联 group 等特性,对象可以通过多条路径到达,这是另一个话题。路径用来做什么?引用!编译器知道每个引用指向哪条路径,这帮助维护内存安全。
每个引用都知道自己指向哪条路径
例如,在这个程序里:
struct Entity { hp int; }
struct World {
entities Vec
world.entities.push(Entity(42));
let entity_ref = &world.entities[0];
... }
entity_ref 的类型是 &Entity in world.entities[]。这意味着它是指向 world 的 entities 中某个元素的引用。
再看之前的例子:
struct World {
entities Vec
你可以看到 entity in world.entities[],指定 entity 参数指向 world 的 entities 中某个元素。如果你熟悉 Rust:路径不太像生命周期,最好把它们当成不同的东西来理解;生命周期更像一段指令区间,而路径是更细的位置。
用路径保证内存安全
编译器用路径来检测 use-after-free,也就是检测我们在解引用一个指向已销毁数据的引用。它靠两条规则:
- 修改一个对象时,把所有指向该对象内部的引用标记为“已失效”。
- 试图使用“已失效”引用时,编译器报错。
看这个 main 函数:
struct Entity { hp int; }
struct World {
entities Vec
world.entities.push(Entity(42)); let first_ref = &world.entities[0];
world.entities = Vec
// Compile error: Used a borrow after invalidated return first_ref.hp; }
里面发生了两件事:在 world.entities = ... 时,编译器让所有指向 world.entities[] 内部的引用失效,也就是让 firstref 失效;在 return firstref.hp 时,编译器看到我们在用 first_ref,于是报错。
注意我说的是:修改一个对象时,我们失效的是指向该对象“内部”的引用,而不是指向该对象“本身”的引用。看另一个 main 函数:
struct Ship { fuel int; }
func main() int {
let vec = Vec
let vecref = &vec; let elemref = &vec[0];
// Mutate vec.
// Don't invalidate vec_ref.
// Invalidate elem_ref.
vec.push(Ship(73));
// Can still use vec_ref.
vec_ref.push(Ship(81));
// Can't use elem_ref,
// Compile error: Used a borrow after invalidated
return elem_ref.fuel;
}
调用 push 修改了 vec,于是让所有指向 vec 内部的引用失效,也就是 elemref 失效,而 vecref 仍然有效。所以我们还能用 vecref,但不能用 elemref。
注意,这一切并没有描述引用是否可变。Valen 只有一种借用引用。如果你来自 C++ 或 Rust,这可能有点困惑:C++ 有 const 和非 const,Rust 有 & 和 &mut。在 Valen 里,可变性不是类型的一部分,由外围函数决定。Valen 确实在函数参数上写 mut,但那是语法糖;mut 不是参数类型的一部分,它属于函数签名,就像语言把 async 放在函数上而不是参数上。
函数签名传达失效信息
编译器怎么知道 push 修改了 vec?push 必须在签名里用 mut 说明。push 的签名是:
func push
它是下面写法的语法糖:
func push
注意 mut(self) 不是 self 类型的一部分,它更属于函数。当 main 调用 vec.push(...) 时,把 vec 传给 self。编译器看到 mut(self),知道 self 其实就是 vec,于是知道 vec 被修改了,因此让指向 vec 内部的引用失效。
再看这个函数:
func step(world &World, entity in world.entities[] mut) { entity.advance(); let collision = world.getcollisionfor_entity(entity); entity.resolve(collision); }
我们不是修改整个 world,只修改 world.entities[] 里的东西。这很好,因为它不会让调用者指向 entities 的引用失效。例如,stepandread 调用上面的 step,而 step 不会让 entity 失效:
func stepandread(world &World, entity in world.entities[] mut) { // Only modifies an entity's contents. step(world, entity);
// ...so can still use entity!
entity.read_book();
}
如果我们把 Valen 编译器做对,并给 LLVM 打补丁,这可能让我们以前所未有的方式利用优化器。简单来说:如果优化器知道两个函数只在读上重叠、而不在写上重叠,它就能重排它们,甚至可能把计算提到循环外。
分组借用的新变化
最初的分组借用设计已经包含了上面很多内容。下面是 Valen 会额外加上的东西,让它真正出彩。
可变中的不可变
回想这个程序:我们接收一个 world,它除了 entities 元素之外是不可变的。
struct World {
entities Vec
我们接收了两个参数引用,可以通过更具体的那个来修改,不能修改它之外的任何东西。Valen 加入这个特性,是为了不用再做“把整个 world 可变地传进来”这种典型的借用检查模式。
这有两个好处。第一,它有助于程序架构,能做出更强的 API:我们可以强制这个函数里没有任何东西能改变我们不期望被改变的 world 部分。第二,我怀疑这个特性能让 Valen 代码优化得更好:当你给优化器更细粒度的信息,告诉它什么可能会变、什么不会变,它就能更好地找出可以重排代码以变得更快的地方。这可能还需要给 LLVM 打补丁,因为 LLVM 倾向于用 noalias 思考,而不是“只读”。不过这不一定意味着 Valen 会比 C++、Rust 更快;在那些语言里,你通常可以通过额外重构或用 unsafe 来获得同等加速。Valen 这里的好处主要是易用性和灵活性,不一定是速度。
临时唯一性
Valen 的目标之一是能无缝调用 Rust 代码,就像 Kotlin 调用 Java、C++ 调用 C 那样容易。
这很棘手,因为分组借用和 Rust 有不同的引用种类:
- 分组借用允许别人修改你的引用所指向的数据。
- 在 Rust 里,每个引用要么是共享的 &,要么是独占的 &mut;当你持有引用时,别人不能修改它指向的东西。
于是问题来了:我们能把一个分组借用引用临时变成 Rust 引用吗?答案是能。机制很简单:当 Valen 调用 Rust 时,
- 如果某个 Valen 引用在调用期间是唯一能触达那份数据的引用,我们就可以把它交给 Rust 的 &mut。
- 如果有多个 Valen 引用可能指向同一个对象,它们就只能交给 Rust 的 & 引用。
这种临时转换将来可能演变成更宽泛的特性。我们想让 Valen 支持类似 Rust 的 Vec<&mut Ship>,其中每个元素都是独占引用,而分组借用本身没有这个概念。我怀疑 Valen 可以有某种“唯一 group”,比如在函数上标注 unique(world.ships[]),阻止创建指向该层级部分的新引用。不过这需要更多思考,因为 Valen 最大的优势之一是只有一种借用引用,引入第二种“唯一”引用必须谨慎。
通配后代路径
这是另一个帮助 Valen 调用 Rust 代码的特性。在尝试让 Golden Spike 工作时,我遇到了一个难题:当 Valen 调用一个返回引用的 Rust 函数(比如下面的 ->&Gem)时,它怎么知道这个引用指向哪里?
impl Chest { pub fn gem(&self) -> &Gem { &self.gem } }
Valen 被这个搞糊涂了,因为 Valen 喜欢知道每个引用的路径。在 Valen 的理想世界里,它希望把上面代码看成:
func gem(self &Chest) -> &Gem in self.gem { &self.gem }
但 Valen 没法从 Rust 代码推断出 in self.gem。不过稍微拉远看,这段 Rust 代码想表达的是:它返回的是 self“内部某处”的东西。
于是我们把这个概念加进 Valen!Valen 现在把上面的 Rust 函数解释成:
func gem(self &Chest) -> &Gem in self.gem... { &self.gem }
in self.gem... 表示“指向 self.gem 内部的某处”。这叫做通配后代路径,这名字确实还需要改进。
我怀疑它不止对 Rust 互操作有用:它还能擦除签名里的内部细节,让库的 API 更灵活、更向前兼容。不过这方面也要谨慎,因为同样的目的在理论上也可以用其他特性实现,比如让一个类型以公开名字重新导出一个私有 group,也就是所谓的“关联 group”。
这一切如何融入
上面我提到了“内存安全圣杯”,以及要找到它必须解决的三个挑战。Valen 的借用检查器是第一点的关键:它让我们能对任何对象持有多个引用,并且能通过其中任何一个读写。
第二点和第三点会放在另一篇帖子里,但我忍不住先给一个快速预览。回想上面的例子:
struct World {
entities Vec
我希望用户在不想指定路径时,可以选择用引用计数的 class,像这样:
class World {
entities Vec
能给用户这样简化的自由,会很棒。当然,把引用计数和借用混合起来是出了名的困难,原因和 Rc
答案藏在我在 2023 年做过的一个叫 Arrrlang 的思想实验里(名字是个玩笑)。它的前提是:把堆表示成 N 个全局数组,N 是程序里类型的数量。这是那个“零成本”语言其实也有成本的例子,成本形式为 ID 追踪和边界检查。
我突然想到:分组借用理解数组。而引用计数可以看成 N 个互相引用的数组。如果把这两个事实放在一起,关键洞察就出现了:如果我们把引用计数的 class 描述给借用检查器时,把它当成一堆数组,那我们也许就能以内存安全的方式拥有指向它们的借用引用。
编译器当然不需要真的把引用计数对象编译成数组,但它可以用分组借用的术语向借用检查器这样描述。如果这样做,很多问题突然就消失了,也澄清了我们需要给分组借用加上的构建块:
- 一个“多类型 group”,类似 Vale 的 region,而不是普通分组借用那样每个 group 都需要一个类型。这对枚举也有用。有趣的是,Iris 在底层就是这样表示堆的,而 Iris 曾被用来形式化验证 Rust 的部分安全性。
- 一个“运行时 group”。指向运行时 group 的引用不会被失效。
就像我说的,这些现在还看不懂。请留意下一篇讲所有这些的帖子!
暂时就到这里
在接下来的几篇文章里,我会解释更多关于如何融入引用计数和分代引用,以及 Valen 的分组借用检查器如何实现。这只是冰山一角。
附录:类型稳定例外
这部分在 Valen 里还没实现。分组借用可以聪明到:如果你写 myvec.push(42),尽管 push 说了 mut(self),它也不会让指向 myvec.size 的指针失效。
这说得通,因为无论谁怎么修改一个 Vec,my_vec.size 那里的东西都还在;没人能把 Vec 的 size 改成整数以外的东西,也就是说它是“类型稳定”的。推广一点:分组借用永远不会让任何类型稳定的数据失效。它不会让指向内含类型稳定东西(基本类型、结构体、定长数组)或它们内部类型稳定东西的指针失效;只会让指向类型不稳定东西(枚举内容、指针所指之物)内部的指针失效。
例如,下面程序里,没人修改 Level 的内容能让指向其 Terrain 的引用失效:
struct Level {
terrain Terrain;
entities HashMap
// Doesn't invalidate terrainref! level.addentity(Entity(1, Loc(4, 5), 42));
print(terrain_ref.tiles.len()); }
评论
讨论气氛是肯定与追问并存:部分人称赞这个路径/分组借用想法开眼界、在代码驱动场景下可能更优,但更多回复是在质疑和澄清它的命名、并发语义以及与 Rust 借用限制的关系。分歧主轴集中在命名应叫 Path 还是 Group、文章是否误把 Rust 的共享异或可变限制当成缺陷、多线程下如何保证跨区域不可变,以及把修改路径写进函数签名是否会损害抽象与零成本抽象。
认为“Path Borrowing”比“Group Borrowing”更贴切
- 多人觉得“group”会让读者去找一个并不存在的 group 类型,而“path”更直接表达借用的本质是访问/修改某条字段路径;作者本人也承认 Groups 并非理解核心,并提出可改名为 Path Borrowing。(较多人)
- > 这听起来更像 path 借用,不过想法很棒、挺开眼界(@kvark)
认为子借用语法应统一用 & 而非 in 关键字
- 作者与另一位评论者都建议放弃 entities in world.entities[] 里的 in,改用 entities: &world.entities[] 或 entity &world.entities[?] 这类带 & 的写法;这样不仅能和借用符号保持一致,还能顺带表达受影响的索引或范围约束。(少数人)
- > 为什么非要用 in,写成 entity &world.entities[?] 不是更一致,还能对受影响的索引范围做约束吗?(@kvark)
认为文章误把 Rust 的“共享异或可变”限制说成问题
- 有人反驳文章把 Rust 的“共享异或可变”限制说成缺陷:该限制并非关于生命周期,去掉也不会影响释放后使用问题,它的价值是带来“无畏并发”,让人免于考虑某些并发 bug;它确实挡掉了一些完全合法的程序,所以大家希望有更好方案,但直接移除并不是更好的办法,文章只是误读了它。(少数人)
- > 这个限制让你不必考虑某些并发 bug,正是“无畏并发”的来源;去掉它并不是更好的方案,文章误读了它(@zamalek)
质疑该方案在多线程下如何保证跨区域的不可变性
- 有人指出:在单线程里,编译器看得到函数签名和可变引用,能判断某条路径及其后代没有被修改;但多线程下(尤其调用进 Rust 的线程)似乎需要一种跨整个代码区域保证对象不可变的机制,例如“完全冻结”引用,才能维持不可变承诺。另有评论直接用示例代码追问:world 是只读是否意味着其它实体在别的线程里也不能被改,以及这段代码是否根本不能并发执行。(少数人)
- > 多线程下难道不需要一种跨代码区域保证对象不可变的方式吗,Valen 里是怎么处理的?(@amluto)
认为把修改路径写进函数签名会损害抽象与零成本抽象
- 有人强调 C++ 和 Rust 都把零成本抽象作为根本设计原则,而“函数修改了哪些字段路径”这种签名依赖结构体的具体实现;一旦把字段隐藏在取值/赋值方法之后,就无法再谈论该函数修改了哪些路径。同一评论还批评文章过了将近一半才第一次尝试定义 group borrowing,前半篇被指废话太多。(少数人)
- > 结构体字段路径依赖具体实现,一旦用取值/赋值方法隐藏字段,函数“修改了哪些路径”就说不清了(@octoberfranklin)
批评示例缺少控制流且沿用过时的游戏世界/实体模型
- 有人指出示例里没有任何控制流,难以据此判断方案;另一条更尖锐的反馈认为,用游戏开发里的 world/entity 示例完全误导,因为业界早已转向数据驱动世界,应把 entity 的 advance 理解成蓝图的执行;不过该方案在代码驱动、静态可知所有路径的系统(如渲染)里可能反而更好。(少数人)
- > 用游戏开发里的 world 和 entity 来举例完全误导,大家都已转向数据驱动世界了(@SleepyMyroslav)
好奇该机制能否反向移植或改造到现有语言
- 有人问这种借用检查器能否反向移植到 Rust,或未来在某个新 edition 中提供;也有人问能否改造进 FreePascal、D、FreeBasic 甚至 Java 等语言。(少数人)
有价值的补充
- Mojo 的 origin 被认为能表达不少这类语义,且社区从中借鉴了 Nick 的提案(尽管没有直接实现);有人还给出 Godbolt 示例,展示两个引用如何更新同一个列表。
- amluto 补充设想:Valen 类语言可能需要三种引用风味——冻结引用(不可变且承诺别人也不会改)、独占引用(像 Rust 的 &mut,完全可变)、以及博文里的灵活引用;他还顺带设想 Rust 若增加“无访问”引用(只保证对象存在、不能读写,可与共享/可变引用共存)也许能让某些 cell 类型更好理解,但自己也还没找到足够理由。
- Valen 是 Verdagon 的另一个独立项目,并不是此前 Vale 语言的改名;原文已澄清这一点。