Here is a code I think should not be accepted without warnings by the compiler:
let mut i = 0;
let mut f = move || {
i += 1;
i
};
println!("{}", f());
println!("{}", i);
I've discussed this quite a lot and some people agree with me and some say "but you said move, its behavior is absolutely expected". Well in reality it it more something like this:
fn main() {
let mut i = 0;
let rc = std::rc::Rc::new(10);
let mut f = {
let rc = rc.clone();
move || {
i += *rc;
i
}
};
println!("{}", f());
println!("{}", i);
}
Now the thing is that you have move
required to make Rc
work, but you actually captured i
by value unintentionally. Note that I'm only talking about mut
bindings that get mutated within closure - immutable copies being moved are absolutely fine. So this being said, I think it's a really rare case when you want to do this, because original code can be always rewritten into:
fn main() {
let i = 0;
let f = move || {
let i = i + 1; // we want to have a personal mutable copy
i
};
println!("{}", f());
println!("{}", i);
}
or
fn main() {
let mut i = 0;
{
let i = &mut i; // you want to have a reference that you mutate in the closure
let f = move || {
*i += 1;
i
};
println!("{}", f());
}
println!("{}", i);
}
But the original code makes a copy which can be easily missed and then mutates it. This remind me of a similar situation in C# where get
property creates a new copy every time you call it and if you're trying to modify it it generates an error, because it's well expected to be a mistake:
class Program {
struct Foo {
public int x;
}
class Bar {
public Foo Foo { get; }
}
static void Main() {
var bar = new Bar();
bar.Foo.x += 1;
// error CS1612: Cannot modify the return value of 'Bar.Foo' because it is not a variable
}
}
I think that compiler should ask for an explicit let
in case if this move was intentional or to create a reference/smart pointer/... in case if it was supposed to modify a shared location. In its current state it looks like a pitfall.
I wanted to end with more realistic example of what I was doing something along these:
#[tokio::main(flavor = "current_thread")]
async fn main() {
let mut i = 0;
let n = 5;
let futures = (0..n).map(|_| async move {
// some dozens of lines of code here
i += 1;
println!("Done {} out of {}", i, n);
});
for f in futures {
f.await;
}
}
The thing is that counter didn't exist in the fist place - it was just bunch of futures with their own work. So I added a counter - and while it stroke me a little bit "why does it compile? Don't I need some kind of syncronization" I immediately thought "well I'm running on a current-thread executor, so it figured out I don't need syncronization and regular reference is enough, since compiler is happy then it most definitely should be valid". And it's easy to imagine I was very wrong about that. I agree that I was tired and probably should have looked better, but in the end I really think that compiler should warn about such issues, C# compiler does just this in a very similar situation. I think this is the best approach and doesn't harm anyone - rust always has been about explicitness so I think this is a great place to apply it.
Hope this all makes sense.