I'm wondering what the difference is between:
"some string".to_string()
And
"some string".into_string()
The former seems to come from ToString, which is quite clear.
However, the latter seems to comes from IntoString, which is less clear to me.
What does it mean to consume
a value ? What is the difference between the two traits ?
Additional information after doing some digging around.
Here's the current implementation of into_string
for a String
. As you can see, it only returns itself, so no allocation is done.
What does it mean to
consume
a value?
Consuming a value has to do with moving a value. Before I discuss the differences between the two traits, I'll give some examples of what it means to move a value. Let's create a Vec
of Ascii
characters: asciis
.
fn main() {
let asciis = vec!['h'.to_ascii(), 'i'.to_ascii()];
println!("{}", asciis);
}
Internally, the Vec
is a struct with three fields:
Vec
.Vec
.Vec
.Pictorially, the memory layout of the Vec
and the data it is managing may look something like this.
Stack: asciis Heap:
+----------+ +----------+
0xF0 | data | ----> 0xA0 | 'h' |
+----------+ +----------+
0xF4 | length | 0xA1 | 'i' |
+----------+ +----------+
0xF8 | capacity |
+----------+
When our Vec
goes out of scope, it frees the memory it's managing. The freed memory is garbage to us. It would be erroneous to access freed memory. This would look something like the following. The Vec
is gone and the memory on the heap has been freed.
Heap:
+----------+
0xA0 | GARBAGE |
+----------+
0xA1 | GARBAGE |
+----------+
Now, let's go back to our code and try to make a copy of asciis
.
fn main() {
let asciis = vec!['h'.to_ascii(), 'i'.to_ascii()];
{
let an_attempted_copy = asciis;
}
println!("{}", asciis);
}
Let's guess that an_attempted_copy
is a copy of asciis
. After we make the copy, our memory may look something like the following.
Stack: asciis Heap: Stack: an_attempted_copy
+----------+ +----------+ +----------+
0xF0 | data | ----> 0xA0 | 'h' | <---- 0xE0 | data |
+----------+ +----------+ +----------+
0xF4 | length | 0xA1 | 'i' | | length |
+----------+ +----------+ +----------+
0xF8 | capacity | | capacity |
+----------+ +----------+
Right before we try to println!
asciis
, an_attempted_copy
goes out of scope! Just like before, the data pointed to by our Vec
is freed.
Stack: asciis Heap:
+----------+ +----------+
0xF0 | data | ----> 0xA0 | GARBAGE |
+----------+ +----------+
0xF4 | length | 0xA1 | GARBAGE |
+----------+ +----------+
0xF8 | capacity |
+----------+
Uh oh, asciis
is pointing into freed memory! This is bad news since we're just about to println!
asciis
.
So how would we remedy the situation? Well, here's two options.
asciis
into an_attempted_copy
, we could copy the data pointed to by asciis
into a freshly allocated piece of memory. Other languages like C++ do this.asciis
, we can move it! This is what rust does. So what does it mean to move? It means that an_attempted_copy
will take ownership of the data previously pointed to by asciis
. asciis
loses ownership and we can't use it anymore. Let's rename an_attempted_copy
for clarity.
fn main() {
let asciis = vec!['h'.to_ascii(), 'i'.to_ascii()];
{
let actually_a_move = asciis;
}
println!("{}", asciis);
}
Now, let's draw our memory layout right after we move into actually_a_move
.
Stack: asciis Heap: Stack: actually_a_move
+----------+ +----------+ +----------+
0xF0 | GARBAGE | 0xA0 | 'h' | <---- 0xE0 | data |
+----------+ +----------+ +----------+
0xF4 | GARBAGE | 0xA1 | 'i' | | length |
+----------+ +----------+ +----------+
0xF8 | GARBAGE | | capacity |
+----------+ +----------+
asciis
doesn't own the memory anymore, so we can't use asciis
anymore. This means it's pretty much garbage. So if we can't use asciis
anymore, what happens when we println!
it? We get the following error.
<anon>:6:24: 6:30 error: use of moved value: `asciis`
<anon>:6 println!("{}", asciis);
^~~~~~
note: in expansion of format_args!
<std macros>:2:23: 2:77 note: expansion site
<std macros>:1:1: 3:2 note: in expansion of println!
<anon>:6:9: 6:32 note: expansion site
<anon>:4:17: 4:32 note: `asciis` moved here because it has type `collections::vec::Vec<std::ascii::Ascii>`, which is moved by default (use `ref` to override)
<anon>:4 let actually_a_move = asciis;
^~~~~~~~~~~~~~~
error: aborting due to previous error
As expected, the rust compiler is telling us we were trying to use ascii
, but ascii
was a moved value; this is erroneous.
Move semantics (and related topics like borrowing and lifetimes) is tough stuff. I've only barely scratched the surface here. For more information, rust by example and this stackoverflow question are both good resources.
to_string
vs into_string
What is the difference between the two traits?
Now that I've explored the concept of consuming or moving a value, let's get to the differences between the two traits. Let's first look at the type signature of to_string
.
fn to_string(&self) -> String;
This function takes a reference to self
and returns a fresh String
for us to use. I haven't discussed references and how they affect movement, but trust me when I say no moving is done here.
Now let's look at the type signature of into_string
.
fn into_string(self) -> String;
This function does not take a reference to self
. Instead, self
is moved into the function.
So what are the implications of this difference? Let's take a look at an example.
fn main() {
let asciis = vec!['h'.to_ascii(), 'i'.to_ascii()];
let no_moves_here = asciis.to_string();
println!("{}", asciis);
}
We again create a Vec
of Ascii
characters. Then, when we call asciis.to_string()
, a brand new String
is created and asciis
is never moved. This code will build and run as you expect, printing out [h, i]
. Now, let's use into_string
.
fn main() {
let asciis = vec!['h'.to_ascii(), 'i'.to_ascii()];
let uh_oh_we_just_moved_asciis = asciis.into_string();
println!("{}", asciis);
}
Here's the error message we get when trying to build this code.
<anon>:4:24: 4:30 error: use of moved value: `asciis`
<anon>:4 println!("{}", asciis);
^~~~~~
note: in expansion of format_args!
<std macros>:2:23: 2:77 note: expansion site
<std macros>:1:1: 3:2 note: in expansion of println!
<anon>:4:9: 4:32 note: expansion site
<anon>:3:42: 3:48 note: `asciis` moved here because it has type `collections::vec::Vec<std::ascii::Ascii>`, which is non-copyable (perhaps you meant to use clone()?)
<anon>:3 let uh_oh_we_just_moved_asciis = asciis.into_string();
^~~~~~
error: aborting due to previous error
So what happened? Well asciis
is being moved into the function into_string
. Just like the last time we tried to use asciis
after we moved it, the rust compiler will reject our code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With