It seems that I cannot mutate anything if there is any immutable reference in my chain of dereferencing. A sample:
fn main() {
let mut x = 42;
let y: &mut i32 = &mut x; // first layer
let z: &&mut i32 = &y; // second layer
**z = 100; // Attempt to change `x`, gives compiler error.
println!("Value is: {}", z);
}
I'm getting the compiler error:
error[E0594]: cannot assign to `**z` which is behind a `&` reference
--> src/main.rs:5:5
|
4 | let z: &&mut i32 = &y; // second layer
| -- help: consider changing this to be a mutable reference: `&mut y`
5 | **z = 100; // Attempt to change `x`, gives compiler error.
| ^^^^^^^^^ `z` is a `&` reference, so the data it refers to cannot be written
In some way, this makes sense, as otherwise the compiler would not be able to prevent having multiple mutable access paths to the same variable.
However, when looking at the types, the semantics seem to be counter-intuitive:
y
has type &mut i32
, or in plain English "A mutable reference to an integer".z
has type &&mut i32
, or in plain English "An immutable reference to a mutable reference to an integer".z
once (i.e. *z
) I will get something of type &mut i32
, i.e. something of the same type as y
. However, dereferencing this again (i.e. **z
) gets me something of type i32
, but I am not allowed to mutate that integer.In essence, the types of references in some sense lie to me, as they don't actually do what they claim they do. How should I read types of references properly in this case, or how else can I restore faith in that concept?
Testing with this sample:
fn main() {
let mut x = 42;
let y: &mut i32 = &mut x; // first layer
let m: &&mut i32 = &y; // second layer
let z: &&&mut i32 = &m; // third layer
compiler_builtin_deref_first_layer(*z);
}
fn compiler_builtin_deref_first_layer(v: &&mut i32) {
compiler_builtin_deref_second_layer(*v);
}
fn compiler_builtin_deref_second_layer(w: &mut i32) {
println!("Value is: {}", w);
}
The parameter types of those last two functions are correct. If I change any of those, the compiler will complain about mismatched types. However, if I compile the example as-is, I get this error:
error[E0596]: cannot borrow `**v` as mutable, as it is behind a `&` reference
Somehow, the call to compiler_builtin_deref_first_layer
seems to be okay, but the call to compiler_builtin_deref_second_layer
isn't. The compiler error talks about **v
, but I only see a *v
.
Back to Rust. A mutable reference is a borrow to any type mut T , allowing mutation of T through that reference. The below code illustrates the example of a mutable variable and then mutating its value through a mutable reference ref_i .
There is no single reason that bindings are immutable by default, but we can think about it through one of Rust's primary focuses: safety. If you forget to say mut , the compiler will catch it, and let you know that you have mutated something you may not have intended to mutate.
If you have a mutable borrow to it, you can modify it through b , by dereferencing it. E.g. to set it to another string, write: *b = String::from("another string"); However, it is a feature of Rust that you can only ever change a value from one place.
Syntax. An ampersand ( & ) is used with a variable's name while passing its reference instead of its value to a function. The function's signature should also have an ampersand with the type of argument that receives the reference.
In essence, the types of references in some sense lie to me, as they don't actually do what they claim they do. How should I read types of references properly in this case, or how else can I restore faith in that concept?
The right way to read references in Rust is as permissions.
Ownership of an object, when it's not borrowed, gives you permission to do whatever you want to the object; create it, destroy it, move it from one place to another. You are the owner, you can do what you want, you control the life of that object.
A mutable reference borrows the object from the owner. While the mutable reference is alive, it grants exclusive access to the object. No one else can read, write, or do anything else to the object. A mutable reference could also be called an exclusive reference, or exclusive borrow. You have to return control of the object back to the original owner, but in the meantime, you get to do whatever you want with it.
An immutable reference, or shared borrow, means you get to access it at the same time as others. Because of that, you can only read it, and no one can modify it, or there would be undefined results based on the exact order that the actions happened in.
Both mutable (or exclusive) references and immutable (or shared) references can be made to owned objects, but that doesn't mean that you own the object when you're referring to it through the reference. What you can do with an object is constrained by what kind of reference you're reaching it through.
So don't think of an &&mut T
reference as "an immutable reference to a mutable reference to T", and then think "well, I can't mutate the outer reference, but I should be able to mutate the inner reference."
Instead, think of it as "Someone owns a T
. They've given out exclusive access, so right now there's someone who has the right to modify the T
. But in the meantime, that person has given out shared access to the &mut T
, which means they've promised to not mutate it for a period of time, and all of the users can use the shared reference to &mut T
, including dereferencing to the underlying T
but only for things which you can normally do with a shared reference, which means reading but not writing."
The final thing to keep in mind is that the mutable or immutable part aren't actually the fundamental difference between the references. It's really the exclusive vs. shared part that are. In Rust, you can modify something through a shared reference, as long as there is some kind of inner protection mechanism that ensures that only one person does so at a time. There are multiple ways of doing that, such as Cell
, RefCell
, or Mutex
.
So what &T
and &mut T
provide isn't really immutable or mutable access, though they are named as such because that's the default level of access they provide at the language level in the absence of any library features. But what they really provide is shared or exclusive access, and then methods on data types can provide different functionality to callers depending on whether they take an owned value, an exclusive reference, or a shared reference.
So think of references as permissions; and it's the reference that you reach something through that determines what you are allowed to do with it. And when you have ownership or an exclusive reference, giving out an exclusive or shared reference temporarily prevents you from mutably accessing the object while those borrowed references are still alive.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With