Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between ToString and IntoString

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.

like image 290
conradkleinespel Avatar asked Aug 14 '14 19:08

conradkleinespel


1 Answers

Move Semantics

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:

  1. The length of the Vec.
  2. The capacity of the Vec.
  3. A pointer to the data managed by the 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.

  1. When we copy 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.
  2. Instead of copying 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.

like image 91
mwhittaker Avatar answered Oct 16 '22 21:10

mwhittaker