注意事项 译者:该记录的日期为2014年5月13日,因此某些细节(包括源代码)可能与当前情况不符。 为什么需要翻译这么长的帖子这一问题的答案将是其内容的价值,以形成对Rust语言的基本概念之一的理解,例如流利程度。
随着时间的流逝,我深信放弃在Rust中可变和不变的局部变量之间的区别会更好。 至少许多人对此问题表示怀疑 。 我想在公开场合陈述自己的立场。 我将给出各种动机:哲学上的,技术上的和实践上的,以及当前系统的主要辩护。 (注意:我将其视为Rust RFC,但认为该色调对于博客帖子而言更好,并且我现在没有时间重写它。)
解说
我非常有决定性地写了这篇文章,并相信我所捍卫的路线是正确的。 但是,如果我们还没有完成对当前系统的支持,那将不是灾难或类似的事情。 它有其优点,总的来说,我觉得它很有趣。 我只是认为我们可以改善它。
一言以蔽之
我想消除不可变和可变局部变量之间的区别,并将&mut
指针重命名为&my
, &only
或&uniq
(这对我没有影响)。 如果没有关键字mut
。
哲学动机
我想要这样做的主要原因是因为我相信这将使语言更加一致和易于理解。 从本质上讲,这将使我们从谈论可变性转向谈论使用别名 (我将其称为“共享”,见下文)。
可变性成为唯一性的结果:“您始终可以更改拥有唯一访问权限的所有内容。共享数据通常是不可变的,但是如果需要,可以使用某种Cell
类型进行更改。”
换句话说,随着时间的流逝,对我来说很清楚,当您同时使用别名和可变性时,就会出现数据竞速和内存安全性的问题。 解决此问题的一种功能方法是消除可变性。 Rust的方法是删除别名的使用。 这为我们提供了一个可以讲述的故事,将有助于我们弄清楚该故事。
关于术语的注释:我认为我们应该将别名的使用称为分隔 ( 译者注:以下,在“分隔”或“共享所有权”的含义中,到处都使用“共享”代替“别名”,因为“别名”都没有使用, “假名”都无法理解所面临的风险 。 在过去,由于其多线程引用,我们避免了这种情况。 但是,如果/当我们实施我提出的数据并行化 计划时 ,则此含义并不完全不合适。 实际上, 考虑到内存安全性与数据竞速之间的紧密联系 ,我真的很想推广这种内涵。
教育动机
我认为当前的规则比应该理解的更难理解。 例如, &mut T
并不意味着任何共享所有权,这一点并不明显。 另外, &mut T
表示&T
并不暗示由于诸如Cell
类的类型而并非完全准确的任何可变性。 而且就如何称呼它们是不可能的(“可变/不可变链接”是最常见的,但这并不完全正确)。
相反, &my T
或&only T
这样的类型似乎简化了解释。 这是唯一的链接 -自然,您不能强迫其中两个指向同一位置。 可变性是正交的:它来自唯一性,但对于单元格也是如此。 &T
类型正好相反,它是一个共享链接 。 RFC PR#58提供了许多类似的参数。 我在这里不再重复。
实践动机
当前,借用的指针(可以是共享的或可变的+唯一的)与始终是唯一的但可以是可变的或不可变的局部变量之间存在间隙。 最终结果是,用户应在不可直接修改的内容上发布mut
广告。
局部变量不能使用引用建模
发生此现象的原因是链接的表达不如局部变量。 通常,这会阻止抽象。 让我给你一些例子来解释我的意思。 想象一下,我有一个环境结构,该结构存储一个指向错误计数器的指针:
struct Env { errors: &mut usize }
现在,我可以创建此结构的实例(并使用它们):
let mut errors = 0; let env = Env { errors: &mut errors }; ... if some_condition { *env.errors += 1; }
好的,现在假设我想将修改env.errors
的代码分成一个单独的函数。 我可能会认为,由于env
变量未声明为可变的,因此可以使用不可变的&
链接:
let mut errors = 0; let env = Env { errors: &mut errors }; helper(&env); fn helper(env: &Env) { ... if some_condition { *env.errors += 1;
但是事实并非如此。 问题是&Env
是共享所有权类型( 译者注:如您所知,一次可以存在多个不可变对象引用 ),因此env.errors
出现在允许env
对象单独env.errors
的空间中。 为了使此代码正常工作,我必须将env
声明为可变的,并使用&mut
链接( 译者注: &mut
)告诉编译器, env
在所有权上是唯一的,因为一次只能存在一个可变对象引用,并且排除了数据竞争,但是mut
因为您无法创建对不可变对象的可变引用 ):
let mut errors = 0; let mut env = Env { errors: &mut errors }; helper(&mut env);
之所以会出现此问题,是因为我们知道局部变量是唯一的,但是如果不使其可变,就无法将这些知识放入借用的引用中。
在其他许多地方也会发生此问题。 到目前为止,我们已经以不同的方式撰写了有关此内容的文章,但我仍被我们谈论要中断的感觉困扰,而这根本不应该。
类型检查闭包
在闭包的情况下,我们必须克服这个限制。 在诸如Env
结构中,关闭大多是开放的,但不是完全开放的。 这是因为如果在闭包中通过&mut
使用局部变量,我不希望将局部变量声明为mut
。 换句话说,以一些代码为例:
fn foo(errors: &mut usize) { do_something(|| *errors += 1) }
描述闭包的表达式实际上将创建Env
结构的实例:
struct ClosureEnv<'a, 'b> { errors: &uniq &mut usize }
查看&uniq
链接。 这不是最终用户可以输入的内容。 它表示“唯一但不一定可变”的指针。 这是通过类型检查所必需的。 如果用户尝试手动编写此结构,则必须编写&mut &mut usize
,这反过来将需要将errors
参数声明为mut errors: &mut usize
。
打开包装的瓶盖和程序
我预测此限制对于解包的闭包是个问题。 让我详细说明我正在考虑的设计。 基本上,想法是||
等效于一些实现特征Fn
新结构类型:
trait Fn<A, R> { fn call(&self, ...); } trait FnMut<A, R> { fn call(&mut self, ...); } trait FnOnce<A, R> { fn call(self, ...); }
从今天起,将根据预期的类型选择确切的类型。 在这种情况下,闭包的使用者可以写以下两件事之一:
fn foo(&self, closure: FnMut<usize, usize>) { ... } fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... }
我们...可能想修复语法,也许添加像FnMut(usize) -> usize
这样的糖,或者保存| usize | ->使用等 它不是那么重要,重要的是我们将按价值传递闭包。 请注意,按照当前的DST(动态大小类型)规则,允许按值传递类型作为FnMut<usize, usize>
的参数,因此参数FnMut<usize, usize>
是有效的DST,这不是问题。
另外 :该项目尚未完成,我将在单独的消息中描述所有详细信息。
问题在于,需要&mut
链接来调用闭包。 由于闭包是通过值传递的,因此用户将不得不再次在看起来mut
地方编写mut
:
fn foo(&self, mut closure: FnMut<usize, usize>) { let x = closure.call(3); }
这与上面的Env
示例中的问题相同:此处实际发生的是FnMut
仅需要唯一的链接,但是由于它不是类型系统的一部分,因此它请求可变的链接。
现在我们也许可以以不同的方式解决这个问题。 我们可以做的一种选择是||
该语法不会扩展为“某些结构类型”,而是会扩展为“结构类型或结构类型的指针,如类型推断所指示”。 在这种情况下,调用者可以编写:
fn foo(&self, closure: &mut FnMut<usize, usize>) { let x = closure.call(3); }
我不想说这是世界的尽头。 但这是不断增加的失真中的又一步,我们必须努力保持局部变量和引用之间的差距。
其他API零件
我没有做详尽的研究,但是,当然,这种差异会蔓延到其他地方。 例如,要从Socket
读取数据,我需要一个唯一的指针,因此必须将其声明为可变的。 因此,有时这不起作用:
let socket = Socket::new(); socket.read()
自然,根据我的建议,这样的代码可以正常工作。 如果尝试从&Socket
进行读取,您仍然会收到一条错误消息,但是它会显示类似“无法创建指向共享链接的唯一链接”之类的信息,我个人认为这更容易理解。
但是我们不需要mut
来保证安全吗?
不,一点也不。 如果仅将所有绑定声明为mut
那么Rust程序同样会很好。 编译器完全能够跟踪在任何给定时间正在更改的局部变量-正是因为它们是当前函数的局部变量。 类型系统真正关心的是唯一性。
我在mut
的当前应用规则中看到的含义(并且我不会否认它具有价值)主要是因为它们有助于声明意图。 也就是说,当我阅读代码时,我知道可以重新分配哪些变量。 另一方面,我也花了大量时间阅读C ++代码,并且坦率地说,我从未注意到这是一个主要的绊脚石。 (我花在阅读Java,JavaScript,Python或Ruby上的代码时也是如此。)
的确,有时我会发现错误,因为我将变量声明为mut
忘记了对其进行更改。 我认为我们可以通过其他更积极的检查来获得类似的好处(例如,循环条件中使用的所有变量都不会在循环体内发生变化)。 我个人不记得要面对相反的情况:也就是说,如果编译器说某些东西应该是可变的,那么基本上总是意味着我忘记了mut
关键字。 (想想:您最后一次是通过做一些除重组代码以使更改有效之外的事情来响应有关无效更改的编译器错误的?)
替代品
我看到了当前系统的三种替代方案:
- 我介绍的那一类是您仅丢弃“可变性”并仅跟踪唯一性。
- 其中一种具有三种引用类型:
&
, &uniq
和&mut
。 (如我所写,这实际上是我们今天拥有的类型系统,至少从借阅检查器的角度来看。) 一个更严格的选择,其中始终将非mut变量视为独立的。 这意味着您将必须编写:
let mut errors = 0; let mut p = &mut errors;
您需要将p
声明为mut
,因为否则它将被认为是单独的变量,即使它是局部变量,因此*p
不允许更改*p
。 这种方案的奇怪之处在于,局部变量不允许单独的所有权,我们可以肯定地知道,因为当您尝试创建其别名时,它将移动,而析构函数将在其上启动,依此类推。 也就是说,我们仍然有“拥有”的概念,这与“不允许单独拥有”不同。
另一方面,如果我们描述了这个系统,说可变性是通过&mut
指针继承的,甚至没有卡住共享所有权,这可能是有道理的。
在这三者中,我绝对更喜欢第一名。 它是最简单的,现在我对如何通过保留Rust的特性来简化Rust最为感兴趣。 否则,我优先考虑我们现在拥有的那个。
结论
基本上,我发现有关可变性的当前规则具有一定的价值,但是它们很昂贵。 它们是一种流动的抽象:也就是说,它们讲的是一个简单的故事,实际上事实证明它是不完整的。 当人们从对&mut
反映可变性工作原理的初步理解转变为全面理解时,这会引起混乱:有时仅需要mut
才能确保唯一性,有时不需要mut
关键字即可实现可变性。
此外,为了保持虚构,我们必须谨慎行事,其中mut
表示可变性,而不是唯一性。 我们为借款人增加了特殊情况以检查结账情况。 通常,我们必须使有关&mut
mutability的规则更加复杂。 我们必须将mut
添加到闭包中以便可以调用它们,或者以不太明显的方式打开闭包的语法。 依此类推。
最终,一切都会变成整体上更复杂的语言。 用户不仅应该考虑共享所有权和唯一性,还应该考虑共享所有权和可变性,并且两者都以某种方式被弄乱了。
我认为这不值得。