u32 implements the Copy trait (meaning it can be duplicately by simply copying bits in memory).
When a value has a type that is Copy and you try to move ownership of it, it just gets copied instead, with the original version still being accessible.
This means that using Copy types (like numeric primitives, or booleans, or char) isn't ideal for understanding how ownership works. You would be better off using something like String for these kinds of examples.
That said, if you change your example to use String, it still works, so what gives?!
This is because the print macro does some magic under the hood that allows it to read variables by reference, even if it looks like you're passing them by value (yes, this is a bit confusing).
This example better illustrates a change of ownership:
fn test() -> String {
let x = String::from("foo");
std::mem::drop(x); // here, ownership of 'x' is moved into the 'drop' function
return x;
}
error[E0382]: use of moved value: `x`
--> src/lib.rs:4:12
|
2 | let x = String::from("foo");
| - move occurs because `x` has type `String`, which does not implement the `Copy` trait
3 | std::mem::drop(x);
| - value moved here
4 | return x;
| ^ value used here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | std::mem::drop(x.clone());
| ++++++++
For a function or method, you can tell from the argument type:
Something / self takes ownership,
&mut Something / &mut self takes an exclusive reference, and
&Something / &self takes a shared reference
Macros (foo!(...)) can sort of do whatever they want, so you just have to read their documentation.
The actual details are a little bit more complicated...
Really, functions and methods always take ownership, but they might be taking ownership of a reference type— It's the & and &mut operators that actually borrow a value. This is obscured a bit for method receivers (self), which can have these referencing operators automatically inserted by the compiler to make the types match up.