Strategies for solving 'cannot move out of' borrowing errors in Rust
The rules around references and borrowing in Rust are fairly straight-forward. Given an owned variable, we are allowed to have as many immutable references to that variable as we want. Rust defaults to immutability, so even functions like trim are written in such a way that the result is a reference to the original string:
fn main() {
let name = " Herman ".to_string();
let trimmed_name = name.trim(); // == &[1..n-1]
}
The only caveat is that I cannot move the name
variable anymore. If I try to move name
, the compiler will give me an error: cannot move out of name
because it is borrowed.
fn main() {
let name = " Herman ".to_string();
let trimmed_name = name.trim();
let owned_name = name; // move error
}
The compiler knows that trimmed_name
is a reference to name
. As long as trimmed_name
is still in scope, the compiler will not let us pass name
to a function, reassign it or do any other move operation. We could clone()
the name
variable and then trim it, but we really just want to let the compiler know when we are done borrowing name
. The key word here is scope. If the reference to name
goes out of scope, the compiler will let us move name
because it is no longer being borrowed. Let us wrap the call to trim()
in curly braces to denote a different scope.
fn main() {
let name = " Herman ".to_string();
{
let trimmed_name = name.trim();
}
let owned_name = name;
}
That is simple enough, but let us take it a step further. Suppose we wanted to get back the length of the trimmed string from within our scope. If we do that inside our curly braces, then trimmed_name_len
will no longer exist once we leave that scope.
fn main() {
let name = " Herman ".to_string();
{
let trimmed_name = name.trim();
let trimmed_name_len = trimmed_name.len();
}
println!("Length of trimmed string is {}", trimmed_name_len); // no such variable error
let owned_name = name;
}
Strategies
There are a few ways to deal with this. They all look pretty similar, but have different trade-offs. We can return the value from a scoped block of code:
fn main() {
let name = " Herman ".to_string();
let trimmed_name_len = {
let trimmed_name = name.trim();
trimmed_name.len()
};
println!("Length of trimmed string is {}", trimmed_name_len);
let owned_name = name;
}
This is a cheap and quick way to force the reference to go out of scope. It does not require us to specify parameters or their types nor does it require us to specify the return type. It is not reusable though. We can get some more reuse if we use an anonymous function (or closure):
fn main() {
let name = " Herman ".to_string();
let f = |name: &str| {
let trimmed_name = name.trim();
trimmed_name.len()
};
let trimmed_name_len = f(&name);
println!("Length of trimmed string is {}", trimmed_name_len);
let owned_name = name;
}
A closure requires us to specify parameters and their types, but makes specifying the return type optional. The way this is written, the anonymous function f
is only usable within the function scope. If we want complete reusuability we can use a normal function:
fn len_of_trimmed_string(name: &str) -> usize {
let trimmed_name = name.trim();
trimmed_name.len()
}
fn main() {
let name = " Herman ".to_string();
let trimmed_name_len = len_of_trimmed_string(name.as_ref());
println!("Length of trimmed string is {}", trimmed_name_len);
let owned_name = name;
}
These strategies only work if we are calling immutable functions. We are temporarily keeping the reference to get some other peice of information. This works really well that information is something like implements the Copy
trait, such as numbers or booleans. If we wanted to do something like remove all spaces on a string like "H e r m a n"
then we are mutating the string. We would have to call name.clone()
in order to later move the original name
variable.
Closure Without Parameters
You may have wondered if we really did have to specify parameters when using a closure. If we try to access the name
variable from within the closure, it will create a reference during compile time. That reference will continue to exist, even if we try to remove the closure f
from scope. Example:
fn main() {
let name = " Herman ".to_string();
let f = || {
let trimmed_name = name.trim();
trimmed_name.len()
};
let trimmed_name_len = f();
println!("Length of trimmed string is {}", trimmed_name_len);
let owned_name = name; // move error
}
error: cannot move out of `name` because it is borrowed
let owned_name = name;
^~~~~~~~~~
note: borrow of `name` occurs here
let f = || {
let trimmed_name = name.trim();
trimmed_name.len()
};
note: in expansion of closure expansion
Real World Example
The above examples are pretty contrived. However, you will run into this when you are breaking down functions into smaller parts. In this below example, I was using a find_matches
function that required an input of type &str
. Given a PathBuf
, I needed to call the immutable file_name()
method on it and then convert it to a &str
by calling to_str()
before calling find_matches(file_name)
. In order to return a tuple of (p, matches)
, I had to make sure reference created by file_name
was out of scope. I chose to use a function, but could have use curly braces or a closure as we discussed above.
fn find_matches(s: &str) -> f64 {
// ...
}
fn count_filename_matches(path: &Path) -> f64 {
let file_name = path.file_name()
.and_then(|f| f.to_str())
.unwrap_or_else(|| {
debug!("Unable to determine filename for {:?}", path);
""
});
find_matches(file_name)
}
fn find_filename_matches_in_path(path: &str) -> Vec<(PathBuf, f64)> {
fs::read_dir(path).unwrap()
.map(|p| p.unwrap().path())
.map(|p| {
let matches = count_filename_matches(p.as_ref(), cmd);
(p, matches)
})
.filter(|&(ref _p, matches)| {
matches > 0.0
})
.collect()
}