我已经读过某个地方,在一个具有指针的语言中,编译器无法在编译时完全决定是否所有指针都被正确使用和/或有效(参考活动对象)由于各种原因,因为那样做基本上构成解决停止问题 . 直觉上这并不奇怪,因为在这种情况下,我们能够在编译时推断程序的运行时行为,类似于this related question中所述 .
但是,据我所知,Rust语言要求指针检查完全在编译时完成(没有与指针相关的未定义行为,至少是“安全”指针,并且没有“无效指针”或“空指针”运行时例外) .
假设Rust编译器无法解决暂停问题,那么谬误在哪里呢?
-
与C语言中的原始指针相比, pointer checking isn't done entirely at compile-time, 和Rust的智能指针是否还会引入一些运行时开销?
-
或者Rust编译器是否有可能无法做出完全正确的决策,有时需要Just Trust The Programmer™,可能使用其中一个生命周期注释(具有
<'lifetime_ident>
语法的注释)?在这种情况下,这是否意味着指针/内存安全保证不是100%,仍然依赖程序员编写正确的代码? -
另一种可能性是Rust指针在某种意义上是非"universal"或受限制的,因此编译器可以在编译时完全推断出它们的属性,但它们并不像e那样有用 . G . C中的原始指针或C中的智能指针 .
-
或许这是完全不同的东西,我误解了一个或多个
{ "pointer", "safety", "guaranteed", "compile-time" }
.
3 回答
Disclaimer :我有点匆忙,所以这有点蜿蜒 . 随意清理它 .
语言设计师仇恨的一个狡猾的伎俩基本上是这样的:Rust只能推断
'static
生命周期(用于全局变量和其他整个程序的生命周期事物)和堆栈(即本地)变量的生命周期:它无法表达或推理关于堆分配的生命周期 .这意味着一些事情 . 首先,处理堆分配的所有库类型(即
Box<T>
,Rc<T>
,Arc<T>
)都拥有它们指向的东西 . 结果,他们实际上并不需要生命来存在 .你需要一生的地方就是你在访问智能指针的内容时 . 例如:
在第二行的幕后发生的是这样的:
换句话说,因为
Box
不是魔术,我们必须告诉编译器如何将它变成磨机借用指针的常规运行 . 这就是Deref
和DerefMut
的特征 . 这提出了一个问题:确切地说,heap_ref
的生命周期是什么?答案是在
DerefMut
的定义中(从记忆中因为我赶时间):就像我之前说的那样,Rust绝对不能谈论"heap lifetimes" . 相反,它必须将堆分配的生命周期
i32
绑定到它手头的唯一其他生命周期:Box
的生命周期 .这意味着"complicated"事物没有明确的生命周期,因此必须拥有他们管理的东西 . 当您将复杂的智能指针/句柄转换为简单的借用指针时,您必须引入生命周期,并且通常只使用句柄本身的生命周期 .
实际上,我应该澄清:"lifetime of the handle",我的意思是"the lifetime of the variable in which the handle is currently being stored":生命时间真的是存储,而不是 Value . 这通常是为什么Rust的新人在他们可以执行以下操作时会被绊倒的原因:
"But... I know that the box will continue to live on, why does the compiler say it doesn't?!"因为Rust无法推断堆生命周期,并且必须求助于将
&x
的生命周期绑定到变量x
,而不是它恰好指向的堆分配 .对编译时无法检查的内容进行特殊的运行时检查 . 这些通常在
cell
箱中找到 . 但是一般来说,Rust会在编译时检查所有内容,并且应该生成与C中相同的代码(如果你的C代码没有做未定义的东西) .如果编译器无法做出正确的决定,则会出现编译时错误,告诉您编译器无法验证您正在执行的操作 . 这也可能会限制你知道正确的东西,但编译器却没有 . 在这种情况下,您始终可以转到
unsafe
代码 . 但正如您所正确的那样,编译器部分依赖于程序员 .编译器会检查函数的实现,看它是否完全符合生命周期的说法 . 然后,在函数的调用站点,它检查程序员是否正确使用该函数 . 这类似于类型检查 . C编译器检查您是否返回了正确类型的对象 . 然后,如果返回的对象存储在正确类型的变量中,它将在调用站点进行检查 . 函数的程序员决不会破坏承诺(除非使用了
unsafe
,但是你总是可以让编译器强制执行你的项目中没有使用unsafe
)Rust不断改进 . 一旦编译器变得更聪明,Rust中的更多东西可能合法 .
在C中有一些可能出错的事情:
悬空指针
双免费
空指针
野指针
这些不会发生在安全的Rust中 .
您永远不会有指向不再位于堆栈或堆上的对象的指针 . 这在生命周期的编译时证明了 .
Rust中没有手动内存管理 . 使用
Box
分配对象(类似但不等于C中的unique_ptr
)再次,没有手动内存管理 .
Box
es自动释放内存 .在安全Rust中,您可以创建指向任何位置的指针,但不能取消引用它 . 您创建的任何引用始终绑定到对象 .
在C中有一些可能出错的事情:
C中可能出错的一切
SmartPointers只会帮助您忘记调用
free
. 您仍然可以创建悬空参考:auto x = make_unique<int>(42);
auto& y = *x;
x.reset();
y = 99;
Rust修复了那些:
见上文
只要
y
存在,您就不能修改x
. 这在编译时检查,并且不能被更多级别的间接或结构所规避 .Rust并不能证明你所有的指针都使用得当 . 你仍然可以写虚假的程序 . Rust证明您没有使用无效指针 . Rust证明你永远不会有空指针 . Rust证明你永远不会有两个指向同一个对象的指针,除非所有这些指针都是不可变的(const) . Rust不允许你编写任何程序(因为那将包括违反内存安全的程序) . 现在Rust仍然阻止你编写一些有用的程序,但是有计划允许更多(合法)程序用安全的Rust编写 .
重新讨论关于暂停问题的引用问题中的示例:
上面的C代码看起来像Rust中的两种方式之一:
对于任意
bar
你无法证明这一点,因为它可能会访问全局状态 . Rust很快就会获得const
函数,这些函数可用于在编译时计算内容(类似于constexpr
) . 如果bar
是const
,则证明self.a
在编译时是否设置为1
变得微不足道 . 除此之外,如果没有pure
函数或函数内容的其他限制,您永远无法证明self.a
是否设置为1
或不 .Rust目前不关心您的代码是否被调用 . 它关心在分配期间
self.a
的内存是否仍然存在 .self.bar()
永远无法销毁self
(unsafe
代码除外) . 因此self.a
将始终在if
分支内可用 .Rust引用的大多数安全性都受到严格规则的保证:
如果你拥有一个const引用(
&
),你可以克隆这个引用并传递它,但不能创建一个可变的&mut
引用 .如果存在对对象的可变(
&mut
)引用,则不能存在对该对象的其他引用 .不允许引用超过它引用的对象,并且所有操作引用的函数必须使用生存期注释(如
'a
)声明如何链接来自其输入和输出的引用 .因此,就表达性而言,我们实际上比使用普通原始指针更有限(例如,仅使用安全引用无法构建图形结构),但这些规则可以在编译时有效地完全检查 .
然而,仍然可以使用原始指针,但是你必须在
unsafe { /* ... */ }
块中包含处理它们的代码,告诉编译器"Trust me, I know what I am doing here" . 这就是一些特殊的智能指针在内部执行的操作,例如RefCell,它允许您在运行时而不是编译时检查这些规则,以获得表达能力 .