String slices

Throughout the previous chapters you've seen quite a few string literals being used in the code, like "To-Do" or "A ticket description". They were always followed by a call to .to_string() or .into(). It's time to understand why!

String literals

You define a string literal by enclosing the raw text in double quotes:

let s = "Hello, world!";

The type of s is &str, a reference to a string slice.

Memory layout

&str and String are different types—they're not interchangeable.
Let's recall the memory layout of a String from our previous exploration. If we run:

let mut s = String::with_capacity(5);
s.push_str("Hello");

we'll get this scenario in memory:

      +---------+--------+----------+
Stack | pointer | length | capacity | 
      |  |      |   5    |    5     |
      +--|------+--------+----------+
         |
         |
         v
       +---+---+---+---+---+
Heap:  | H | e | l | l | o |
       +---+---+---+---+---+

If you remember, we've also examined how a &String is laid out in memory:

     --------------------------------------
     |                                    |         
+----v----+--------+----------+      +----|----+
| pointer | length | capacity |      | pointer |
|    |    |   5    |    5     |      |         |
+----|----+--------+----------+      +---------+
     |        s                          &s 
     |       
     v       
   +---+---+---+---+---+
   | H | e | l | l | o |
   +---+---+---+---+---+

&String points to the memory location where the String's metadata is stored.
If we follow the pointer, we get to the heap-allocated data. In particular, we get to the first byte of the string, H.

What if we wanted a type that represents a substring of s? E.g. ello in Hello?

String slices

A &str is a view into a string, a reference to a sequence of UTF-8 bytes stored elsewhere. You can, for example, create a &str from a String like this:

let mut s = String::with_capacity(5);
s.push_str("Hello");
// Create a string slice reference from the `String`, 
// skipping the first byte.
let slice: &str = &s[1..];

In memory, it'd look like this:

                    s                              slice
      +---------+--------+----------+      +---------+--------+
Stack | pointer | length | capacity |      | pointer | length |
      |    |    |   5    |    5     |      |    |    |   4    |
      +----|----+--------+----------+      +----|----+--------+
           |        s                           |  
           |                                    |
           v                                    | 
         +---+---+---+---+---+                  |
Heap:    | H | e | l | l | o |                  |
         +---+---+---+---+---+                  |
               ^                                |
               |                                |
               +--------------------------------+

slice stores two pieces of information on the stack:

  • A pointer to the first byte of the slice.
  • The length of the slice.

slice doesn't own the data, it just points to it: it's a reference to the String's heap-allocated data.
When slice is dropped, the heap-allocated data won't be deallocated, because it's still owned by s. That's why slice doesn't have a capacity field: it doesn't own the data, so it doesn't need to know how much space it was allocated for it; it only cares about the data it references.

&str vs &String

As a rule of thumb, use &str rather than &String whenever you need a reference to textual data.
&str is more flexible and generally considered more idiomatic in Rust code.

If a method returns a &String, you're promising that there is heap-allocated UTF-8 text somewhere that matches exactly the one you're returning a reference to.
If a method returns a &str, instead, you have a lot more freedom: you're just saying that somewhere there's a bunch of text data and that a subset of it matches what you need, therefore you're returning a reference to it.

Exercise

The exercise for this section is located in 04_traits/06_str_slice