|
There are many other instances where ownership/lifetimes kick in in ways that would not be a problem in a different language. Anywhere you have any kind of containment hiearchy, Rust starts becoming more invasive than other languages. In languages like Go, you can have complex graphs of objects referring to each other, and you can — often arguably safely — work with these structures without thinking about who owns what. As an example of something that got me stuck, I recently had some code that populated a map of mutable buffers: struct Builder {
buffers: BTreeMap<Term, RefCell<PostingBuffer>>,
}
At the end of the building, it needs to flush the buffers, in key order, to a file and then empty the map so it can be reused. Turns out this is trickier than expected because the map owns its contents, so you can't take ownership of the RefCell that wraps the buffers: for (term, buf) in &self.buffers {
// into_inner consumes buf, so it moves; fails with "Cannot move out of borrowed context"
let data = buf.into_inner().get_data();
w.write(term, data)?;
}
self.buffers = BTreeMap::new();
In the end, the trick was to replace the map for the iteration: for (term, v) in std::mem::replace(&mut self.buffers, BTreeMap::new()) {
let data = v.into_inner().get_data();
w.write(term, data)?;
}
Initially I used a HashMap to optimize for build speed, then sorted its keys at the end. This was also a challenge, because maps apparently have no way of getting a copy of the keys by value. (There might be an easier way, but again, this just illustrates the learning curve for new developers.) In the end, I chose BTreeMap so the keys are already sorted.This is all probably entirely obvious to Rust experts, but not so to new developers. Swapping out the entire map with std::mem::replace() would never have occurred to me. Even if you understand borrowing in principle, you have think about a container means in terms of borrowing, and how stuff will move in and out of a container. And you have to design the way you interact with them accordingly. In principle, I think this awareness is a good thing, and that the resultant code will be smarter and more optimal as a result, but at the same time, it doesn't make for such an ergonomic developer experience; so far it's been a drain on my productivity more than a gain. I like to say that Rust "scales down" towards the low levels, but does less well "scaling up"; you can't write high-level code without also thinking about the low levels, when even things like the size of your data type is always in your face. My solution above may not even be the most optimal, idiomatic way of doing things. I'm sure someone will come along and point out that there's a trick involving using something other than RefCell or whatever that simplifies everything. Not necessarily a criticism of Rust, just a data point in terms of complexity and learning curve. |
I think that if you wrote
it should have Just Worked. By iterating over a reference to self.buffers, you iterate over a reference to the contents, which you can't move out of. But iterating over it by value, it should give it to you by value, and you'd be fine. That said, I have not tried it, so there might be some context I'm missing. One nice thing about a strict compiler is that I have to think about the details less, and use the error messages to help me fix any problems I find. But that makes it harder to know how it goes without the compiler...