Given the following program:
struct Data {
pub items: Vec<&'static str>,
}
trait Generator {
fn append(&mut self, s: &str) {
self.output().push_str(s);
}
fn data(&self) -> &Data;
fn generate_items(&mut self) {
for item in self.data().items.iter() {
match *item {
"foo" => self.append("it was foo\n"),
_ => self.append("it was something else\n"),
}
}
}
fn output(&mut self) -> &mut String;
}
struct MyGenerator<'a> {
data: &'a Data,
output: String,
}
impl<'a> MyGenerator<'a> {
fn generate(mut self) -> String {
self.generate_items();
self.output
}
}
impl<'a> Generator for MyGenerator<'a> {
fn data(&self) -> &Data {
self.data
}
fn output(&mut self) -> &mut String {
&mut self.output
}
}
fn main() {
let data = Data {
items: vec!["foo", "bar", "baz"],
};
let generator = MyGenerator {
data: &data,
output: String::new(),
};
let output = generator.generate();
println!("{}", output);
}
The following errors are produced trying to compile it:
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
--> src/main.rs:15:26
|
13 | for item in self.data().items.iter() {
| ---- - immutable borrow ends here
| |
| immutable borrow occurs here
14 | match *item {
15 | "foo" => self.append("it was foo\n"),
| ^^^^ mutable borrow occurs here
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
--> src/main.rs:16:22
|
13 | for item in self.data().items.iter() {
| ---- - immutable borrow ends here
| |
| immutable borrow occurs here
...
16 | _ => self.append("it was something else\n"),
| ^^^^ mutable borrow occurs here
What is the proper way to structure the code so that the mutable field output
can be written to while iterating over the immutable field data
? Assume the indirection through the Generator
trait is being used to share similar logic with other structs, so accessing MyStruct
's fields from the trait's default method implementations need to be done through accessor methods like this.
This is a common issue in Rust; the typical way of solving it is the replace dance. This involves making more of the data and methods use mutable references:
struct Data {
pub items: Vec<&'static str>,
}
trait Generator {
fn append(&mut self, s: &str) {
self.output().push_str(s);
}
fn data(&mut self) -> &mut Data;
fn generate_items(&mut self) {
// Take the data. The borrow on self ends after this statement.
let data = std::mem::replace(self.data(), Data { items: vec![] });
// Iterate over the local version. Now append can borrow all it wants.
for item in data.items.iter() {
match *item {
"foo" => self.append("it was foo\n"),
_ => self.append("it was something else\n"),
}
}
// Put the data back where it belongs.
std::mem::replace(self.data(), data);
}
fn output(&mut self) -> &mut String;
}
struct MyGenerator<'a> {
data: &'a mut Data,
output: String,
}
impl<'a> MyGenerator<'a> {
fn generate(mut self) -> String {
self.generate_items();
self.output
}
}
impl<'a> Generator for MyGenerator<'a> {
fn data(&mut self) -> &mut Data {
self.data
}
fn output(&mut self) -> &mut String {
&mut self.output
}
}
fn main() {
let mut data = Data {
items: vec!["foo", "bar", "baz"],
};
let generator = MyGenerator {
data: &mut data,
output: String::new(),
};
let output = generator.generate();
println!("{}", output);
}
The thing to realize is that the compiler is right to complain. Imagine if calling output()
had the side effect of mutating the thing that is referenced by the return value of data()
Then the iterator you're using in the loop could get invalidated. Your trait functions have the implicit contract that they don't do anything like that, but there is no way of checking this. So the only thing you can do is temporarily assume full control over the data, by taking it out.
Of course, this pattern breaks unwind safety; a panic in the loop will leave the data moved out.
Assume the indirection through the
Generator
trait is being used to share similar logic with other structs, so accessingMyStruct
's fields from the trait's default method implementations need to be done through accessor methods like this.
Then it's impossible.
The compiler recognizes access to different fields when it sees such fields directly; it does not break abstraction boundaries to peek inside the functions called.
There have been discussions about adding attributes on the methods to specifically mention which field is accessed by which method:
however... this is for non-virtual methods.
For a trait this gets significantly more complicated because a trait does not have fields, and each implementer may have a different set of fields!
So now what?
You will need to change your code:
append
method, forcing users to use interior mutabilityYou can use RefCell
:
RefCell uses Rust's lifetimes to implement 'dynamic borrowing', a process whereby one can claim temporary, exclusive, mutable access to the inner value. Borrows for RefCells are tracked 'at runtime', unlike Rust's native reference types which are entirely tracked statically, at compile time. Because RefCell borrows are dynamic it is possible to attempt to borrow a value that is already mutably borrowed; when this happens it results in thread panic.
use std::cell::{RefCell, RefMut};
struct Data {
pub items: Vec<&'static str>,
}
trait Generator {
fn append(&self, s: &str) {
self.output().push_str(s);
}
fn data(&self) -> &Data;
fn generate_items(&self) {
for item in self.data().items.iter() {
match *item {
"foo" => self.append("it was foo\n"),
_ => self.append("it was something else\n"),
}
}
}
fn output(&self) -> RefMut<String>;
}
struct MyGenerator<'a> {
data: &'a Data,
output: RefCell<String>,
}
impl<'a> MyGenerator<'a> {
fn generate(self) -> String {
self.generate_items();
self.output.into_inner()
}
}
impl<'a> Generator for MyGenerator<'a> {
fn data(&self) -> &Data {
self.data
}
fn output(&self) -> RefMut<String> {
self.output.borrow_mut()
}
}
fn main() {
let data = Data {
items: vec!["foo", "bar", "baz"],
};
let generator = MyGenerator {
data: &data,
output: RefCell::new(String::new()),
};
let output = generator.generate();
println!("{}", output);
}
Rust playground
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