Mutating one field while iterating over another immutable field

后端 未结 3 874
执笔经年
执笔经年 2020-12-03 18:38

Given the following program:

struct Data {
    pub items: Vec<&\'static str>,
}

trait Generator {
    fn append(&mut self, s: &str) {
             


        
相关标签:
3条回答
  • 2020-12-03 19:17

    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.

    0 讨论(0)
  • 2020-12-03 19:17

    You 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

    0 讨论(0)
  • 2020-12-03 19:29

    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.

    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:

    • the compiler would enforce that a method does not touch any field NOT mentioned in the attribute
    • the compiler could then use the knowledge that said method only operates on a subset of the fields

    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:

    • you can split the trait in two, and require two objects (one to iterate, one to mutate)
    • you can "hide" the mutability of the append method, forcing users to use interior mutability
    • ...
    0 讨论(0)
提交回复
热议问题