为什么Rust中变量的可变性不反映在类型签名中?

9
据我理解,可变性不会反映在变量类型签名中。例如,这两个引用具有相同的类型签名&i32
let ref_foo : &i32 = &foo;
let mut ref_bar : &i32 = &bar;

为什么会这样?这似乎是一个很大的疏忽。我的意思是,即使是C/C++也更加明确地使用两个const来表示我们有一个指向const数据的const指针:
const int * const ptr_foo = &foo;
const int * ptr_bar = &bar;

有更好的思考方式吗?


3
mut 不是用来表示可变性吗? - 463035818_is_not_a_number
1
C++也没有const int& const,这是你想要的吗? - 463035818_is_not_a_number
进一步阐述最后一条评论:你在比较橙子和苹果。Rust的示例是关于引用的(在C ++中没有两个const用于引用),而C ++示例是关于指针的。 - 463035818_is_not_a_number
在C++中,引用是固定的,它们不能被重新分配以引用其他内容。因此,与&运算符相关联的一个const只是指它绑定的数据是否为const - mallwright
3个回答

20

在Rust中,可变性是绑定的属性,而不是类型的属性。

一个值的唯一所有者可以通过将其移动到可变绑定来进行更改:

let s = "Hi".to_owned();  // Create an owned value.
s.push('!');              // Error because s is immutable.
let mut t = s;            // Move owned value to mutable binding.
t.push('!');              // Now we can modify the string.

这表明可变性不是值类型的属性,而是其绑定的属性。当然,如果该值当前被借用,则代码仅在未被借用时才能运行,否则将阻止移动该值。共享借用仍然保证是不可变的。
引用的可变性与绑定的可变性是正交的。Rust使用相同的mut关键字来消除两种类型的引用之间的歧义,但它是一个独立的概念。
内部可变性模式再次与上述正交,因为它是类型的一部分。包含CellRefCell或类似内容的类型即使只持有对它们的共享引用也可以修改。
一种常见的模式是在完成值的变异后将其重新绑定为不可变:
let mut x = ...;
// modify x ...
let x = x;

在Rust中,所有权语义和类型系统与C ++有所不同,我更喜欢Rust的方式。我认为它并不本质上比你所暗示的表达力差。


“引用的可变性与绑定的可变性是正交的。”这一点在书中需要更加清晰地强调。 - mallwright
1
有一件事我想在这个答案中补充的是简洁地说明这行代码中每个 mut 的含义:let mut ref_bar : &mut i32 = &mut bar;。例如,ref_bar 是一个可变的引用,指向一个可变的 i32,它与名为 bari32 (相互)独占绑定。 - mallwright
3
@allsey87 第一个 mut(从左到右阅读)是如上所示绑定的一部分。第二个 mut 属于 &mut,意味着 ref_bar 的类型是某种独特引用(或可变引用,无论你怎么看它),指向某些数据(在本例中为 i32)。最后一个 mut 再次属于 &mut,但这次它是一个运算符,意思是“获取唯一引用”(或可变引用)。&mut 运算符创建 &mut 值,& 运算符创建 & 值。 - Optimistic Peach

4

C++和Rust中的常量本质上是不同的。在C++中,const是任何类型的属性,而在Rust中,它是引用的属性。因此,在Rust中不存在真正的常量类型。

例如,考虑下面这段C++代码:

void test() {
    const std::string x;
    const std::string *p = &x;
    const std::string &r = x;
}

变量x被声明为常量类型,因此对它创建的任何引用都将是常量,并且任何尝试修改它(例如使用const_cast)都将导致未定义行为。请注意,const是对象类型的一部分。

然而,在Rust中,没有声明常量变量的方法:

fn test() {
    let x = String::new();
    let r = &x;

    let mut x = x; //moved, not copied, now it is mutable!
    let r = &mut x;
}

在这里,const或mut不是变量类型的一部分,而是每个引用的属性。即使变量的原始名称也可以视为引用。
因为当您在C ++或Rust中声明局部变量时,实际上正在做两件事:
创建对象本身。
声明一个名称以访问对象,一种引用。
当您编写C ++常量时,您使对象和引用都变为常量。但在Rust中没有常量对象,所以只有引用是常量。如果移动对象,则处理原始名称并绑定到可能可变的新名称。
请注意,在C ++中,您无法移动常量对象,它将永远保持不变。
关于指针有两个常量,如果您有两个间接引用,则在Rust中它们完全相同。
fn test() {
    let mut x = String::new();
    let p: &mut String = &mut x;
    let p2: &&mut String = &p;
}

关于哪个更好,那就是口味问题,但请记住在C++中一个常量可以做的所有奇怪的事情:
  • 一个常量对象总是不变的,除非在构造函数和析构函数中。
  • 一个带有可变成员的常量类并不是真正的常量。mutable不是类型系统的一部分,而Rust的Cell/RefCell是。
  • 一个带有常量成员的类很难使用:默认构造函数和复制/移动运算符无法工作。

这一切都是正确的,但它如何回答标题中的问题?为什么ref_fooref_bar似乎都有类型&i32,即使只有后者可以用作可变的呢? - Bartek Banachewicz
@BartekBanachewicz: 这是因为在C++中,const是类型的一部分(const int&是对const int的引用),而在Rust中,它是引用的属性,&i32 是对i32的常量引用。 - rodrigo
“But in” 指的是什么? - Aiueiia
1
@Aiueiia:你是指段落末尾的"But in"吗?我认为这是复制/粘贴时出现的打字错误,我现在正在修复它。 - rodrigo

0

C ++中,默认情况下所有内容都是可变的,并且const关键字表示要更改该行为。

Rust中默认情况下所有内容都是不可变的,mut关键字表示您要更改该行为。

请注意,对于指针,Rust确实需要mutconst关键字:

let ref_foo : *const i32 = &foo;
let mut ref_bar : *const i32 = &bar;

因此,您的示例是等效的,但 Rust 更简洁,因为它默认为不可变。

即使是 C/C++ 也做得更好

多年的 C++ 和 Rust 开发经验让我相信 Rust 处理可变性的方式(例如默认为不可变,但还有其他差异)要好得多。


这并没有回答为什么声明ref_barmut的事实似乎没有被观察到的问题。 - Bartek Banachewicz
你的意思是它没有被观察到? - mcarton
可能在类型签名中使用“reflected”这个词更加恰当,比“observed”更为准确。 - mallwright
@ÖmerErden,我不是指修改指针所指的内容,而是指修改引用本身。 - Bartek Banachewicz
@BartekBanachewicz 在Rust中,绑定的可变性不需要反映在其类型中(如果您知道绑定是可变的,那么在类型层面上你可以做些什么,现在做不到的呢?) - mcarton
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接