Reputation: 7634
I still struggle to grasp why &mut self
is required to modify the internal state of an object owned by my struct. I understand why I have to use at least &self
, because I don't want to consume self
, ending its existance. I also understand why I have to use &mut self
if I were modifying fields of my struct, but I don't.
I have the following struct and implementation:
struct Writer {
obj: json::JsonValue
}
impl<'b> Writer {
fn from(obj: json::JsonValue) -> Self {
Self {
obj
}
}
fn put(&mut self, field_name: &str, value: bool) {
self.obj.insert(field_name, value);
// ^^^- not modifying this, but a field inside "obj"
}
fn release_ownership(self) -> json::JsonValue {
self.obj
}
}
Usage:
writer.put(field_name, true);
With &self
instead of &mut self
I'd get a compiler error:
`^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable`
The Rust book taught me:
If we wanted to change the instance that we’ve called the method on as part of what the method does, we’d use &mut self as the first parameter.
I don't see how I modify the struct here. I clearly modify a field (here obj
) in the struct, but not the struct itself.
In the definition of JsonValue::insert()
I can see &mut self
, which I do not understand either, since it "only" mutates the contents of object
through JsonValue::Object::insert()
.
The Rust book says:
Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable.
I can understand this, if I want to modify field values, but not, if the fields are pointers to other structs that are modified.
My intuition tells me, that me qualifying with &mut
refers not to the direct reference to data, but to the whole path along all pointers or to the final reference. If this is the case: why is that?
However, it seems redundant, since JsonValue::insert()
already enforced mutable access to its internal object. So the compiler knows, that the instance of JsonValue
was mutably borrowed for the insert and can not be borrowed again, until it was released.
Homework: read the book and did my search.
Upvotes: 1
Views: 1226
Reputation: 16910
I guess the question that is bothering you is about the shallow vs deep nature of const-ness/mutability. Depending upon the programming language, this aspect can be seen differently. If you have a background in a language where (almost) everything is accessed via references (Java, JS, Python...), defining a structure with a member referring to something else can be perceived as if the internal details of this member are not part of the structure (they are outside the structure). Consequently, changing something within the internal details of this member can be seen as not mutating the structure.
In C language, this is well known as the shallow constness:
typedef struct {
int a;
int b[3];
} S1;
typedef struct {
int a;
int *b;
} S2;
// ...
const S1 s1 = { 11, { 99, 88, 77 } };
// s1.a = 22; // error, a is altered but s1 is const
// s1.b[0] = 33; // error, b is altered but s1 is const
const S2 s2 = { 11, malloc( ... ) };
// s2.a = 22; // error, a is altered but s2 is const
// s2.b = realloc(s2.b, ... ); // error, b is altered but s2 is const
s2.b[0] = 33; // !!! allowed !!! since b is not altered itself (but what it refers to)
In my opinion this is very confusing.
In C++, standard containers prefer deep constness:
struct S {
int a;
std::vector<int> b;
};
// ...
const S s{ 11, std::vector{99, 88, 77} };
// s.a = 22; // error, a is altered but s is const
// s.b[0] = 33; // error, altering the stored values is an alteration of the member itself
In my opinion this is much more consistent and easy to reason about: « my structure is altered as a whole if I alter any of its parts ».
In Rust, deep constness/mutability applies (as with C++ standard containers).
struct S1 {
a: u8,
b: [u8; 3],
}
struct S2 {
a: u8,
b: Box<[u8; 3]>, // we could have used Vec instead
}
let s1 = S1 { a: 11, b: [99, 88, 77] };
// s1.a = 22; // error, a is altered but s1 is not mut
// s1.b[0] = 33; // error, b is altered but s1 is not mut
let s2 = S2 { a: 11, b: Box::new([99, 88, 77]) };
// s2.a = 22; // error, a is altered but s2 is not mut
// s2.b[0] = 33; // error, b is altered but s2 is not mut
For S1
it's obvious that the array takes place directly inside
the structure, but if we change our mind about the internal details
of the storage, as in S2
, we probably don't want to change the semantics.
This last example shows that the implementation detail about the way we
store the data for b
(array, boxed-array, vector...) does not change the semantics: « my structure is altered as a whole if I alter any of its parts ».
Coming back to your original example, the JsonValue
is a part of your
Writer
structure, then altering this part should be considered as an
alteration of the whole structure.
This is the safer choice by default.
On the other hand, if you have a good reason to use a very broad semantics
(à la Java, JS, Python...) as if the ownership was not clearly defined but
eventually shared between several containers, or as if anyone could modify
any piece of data at any time, then you can always explicitly opt-in with
Rc
and RefCell
.
In my opinion, it is better to start from a very strict and safe semantics
and explicitly opt-in for something less strict if necessary (it's explicit,
anyone can see it in the source code) than the opposite (the language lets
you do anything but you expect nobody will break the strict semantics you
thought about).
Upvotes: 0
Reputation: 27550
Everything along the access chain has to be declared a mutable reference to make sure you can't get two mutable references to the same object. Consider this:
struct ContainsMutable<'a>(&'a mut i8);
fn main() {
let mut a = 99;
let cm = ContainsMutable(&mut a);
let cmref1 = &cm;
let cmref2 = &cm;
// now if both references allowed mutable access to the pointee
// `a` we'd have a problem because this would be allowed
std::thread::scope(|s| {
s.spawn(|| {
*cmref1.0 = 55;
});
s.spawn(|| {
*cmref2.0 = 31;
});
});
}
So we can never be allowed mutable acces to something behind a shared reference (something that we accessed through a shared reference somewhere along the path) because we could easily create mutable references to the same object that way.
However, it seems redundant, since JsonValue::insert() already enforced mutable access to its internal object. So the compiler knows, that the instance of JsonValue was mutably borrowed for the insert and can not be borrowed again, until it was released.
That assumes the compiler can predict at compile time when the function is called and disallow this, but it can't, or that borrow checking is done at runtime, it's not, borrow checking is done statically at compile time. You can get runtime borrow checking with RefCell
, Mutex
or a RwLock
.
Upvotes: 5