Which gives me a compiler error, the gist of it being the use of a moved value:
8 | (s, s.len())
| - ^ value used here after move
| |
| value moved here
And that's what I don't understand. Isn't the function calculate_length a whole scope where s is valid? Why did s moved when constructing the final tuple as expression?
In rust when you pass a variable (and not a reference of a variable) that variable is always moved. In your case it has been moved into the first element of the tuple. The next statement would be to fill in the second part of the tuple and the compiler complains that s has been moved away already. Rust enforces those rules to guarantee that there is only one owner. The original owner was outside of the function. After that the owner became calculate_length and last the owner is the tuples first element.
If you know C++ you can think of String only having a move constructor and no copy constructor.
The u32 also has the copy trait (analog to a C++ copy constructor). Therefore the length can be copied of first but the String has already been moved away.
Rust is just super pedantic about order of evaluation.
When it builds the first argument of the tuple it moves s from your scope into the tuple, so when it comes to evaluate the second argument it sees that s is already "gone" and belongs to the tuple, not the variable.
Yes. Reading this whole section of the book made that clear for me (including running the examples and my own variations). I came to understand this ownership deal with regards to scopes, which I thought I could easily identify, as they're variable visibility perimeters. That's why I thought
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
was just one scope.
And that's what makes my understanding wrong: it seems as if the tuple... is a new scope? I'm trying to wrap my head around this, despite your comment (and @kornel's) I'm not finding this terribly intuitive. Still, thanks!
fn main() {
let message = String::from("Hello, world!");
let population = 7.442;
let tuple = (message, population);
println!("message from tuple: '{}'", tuple.0);
println!(" original message: '{}'", message);
}
Which won't compile, making it obvious that the tuple takes ownership of the variable, just like @Arne wrote. By defining the tuple, I'm invalidating the original variable. What's happening here could be equivalent (-ish) to
fn main() {
let message = String::from("Hello, world!");
let text = message;
println!(" text: '{}'", text);
println!("message: '{}'", message);
}
This won't compile as message was moved to text, which is literally explained earlier in the book. I think I got it.
It's the order of the operations that matters here. Don't think of the tuple as a new scope; rather view your code as a sequence of instructions to the compiler:
Move the function argument s into the first element of the tuple, making the argument inaccessible via future references to the name s.
Take the length of the [no longer accessible] function argument s and move it into the second element of the tuple.
Existence of variables is strictly limited to their scope, but objects owned by these variables can change owners (and the new owner can be a new variable in a new scope, but also a temporary object or something else without an obvious scope).
Ownership has to be tracked more precisely than scope of variables, e.g. loops need to track ownership for each iteration of the loop, even though there's only one scope.
Expression evaluation order is "in order of appearance". In other words Left to Right and Top to Bottom. And each element of a tuple is a different expression which are separated left to right by the comma.