Strings and Text · about 35 min

String and &str

Why this matters

Text is everywhere: input, output, file names, messages, configuration. Rust has two main string types, String and &str, and newcomers are often confused about which to use. With modules 4 and 5 behind you, the distinction is simple: String owns its text, &str borrows it. This lesson makes that precise, shows how to convert between them, and covers building and joining strings.

Two types, one idea

String&str
whatowned, growable text bufferborrowed view of some text
memoryhandle (ptr, len, cap) + heap bufferfat pointer (ptr, len) into someone else's bytes
size of the handle24 bytes16 bytes
can grow or changeyes (if mut)no
created byString::from, to_string, format!, read_line into itliterals, slicing, borrowing a String

This is the same relationship as Vec<T> and &[T] (module 9): an owned growable buffer, and a slice borrowing part of it. Both always hold valid UTF-8 (next lesson).

let lit: &str = "hi";               let s: String = String::from("hello");   let part: &str = &s[1..4];

 lit: ptr --+                        s: ptr  ------------+                   part: ptr --------+
      len 2 |                           len 5            |                         len 3      |
            v                           cap 5            v                                    v
 static data: [h|i]                          heap: [h | e | l | l | o]               (points at 'e')

String literals

A literal like "hi" is stored in the executable's read-only data, loaded at startup and valid for the whole run. Its type is &'static str: a borrowed view of bytes that live forever ('static). That is why you can return a literal from a function with no ownership worries, and why literals cannot be modified.

Converting

fn main() {
    let a: String = String::from("one");
    let b: String = "two".to_string();
    let c: String = "three".to_owned();
    let d: &str = &a;
    let e: &str = a.as_str();
    let f: &str = &b[..2];
    println!("{a} {b} {c} {d} {e} {f}");
}
one two three one one tw

The automatic &String to &str conversion is called deref coercion. It is why a function with a &str parameter accepts &my_string:

fn count_spaces(s: &str) -> usize {
    s.matches(' ').count()
}

fn main() {
    let owned = String::from("a b c");
    println!("{}", count_spaces(&owned));
    println!("{}", count_spaces("x y"));
}
2
1

Guideline: accept &str in parameters, return String when you create new text, store String in structs (owned data, module 7).

Building strings

A String grows like the buffer you saw in module 4:

fn main() {
    let mut s = String::with_capacity(16);
    s.push_str("abc");
    s.push('d');
    s.insert(0, '>');
    s += "ef";
    println!("{s} len={} cap={}", s.len(), s.capacity());
    s.truncate(3);
    println!("{s}");
    s.clear();
    println!("{:?} empty={}", s, s.is_empty());
}
>abcdef len=7 cap=16
>ab
"" empty=true

push_str appends a &str, push a single char, += is push_str. with_capacity preallocates when you know roughly how big the result will be.

Joining strings: +, format! and join

The + operator has an ownership twist:

fn main() {
    let first = String::from("Hello");
    let second = String::from("world");
    let joined = first + ", " + &second;
    println!("{joined}");
    println!("{second}");
}
Hello, world
world

a + b requires an owned String on the left and a &str on the right. It takes ownership of first, appends to its buffer (reusing it, no new allocation if there is room) and returns it. So first is moved and cannot be used afterwards, while second was only borrowed. Two strings cannot be added as &str + &str:

fn main() {
    let a = "Hello, ";
    let b = "world";
    let c = a + b;
}
error[E0369]: cannot add `&str` to `&str`
 --> main.rs:4:15
  |
4 |     let c = a + b;
  |             - ^ - &str
  |             | |
  |             | `+` cannot be used to concatenate two `&str` strings
  |             &str
  |
  = note: string concatenation requires an owned `String` on the left
help: create an owned `String` from a string reference
  |
4 |     let c = a.to_owned() + b;
  |              +++++++++++

The compiler explains that &str values cannot be added and suggests .to_owned() on the left side.

For anything beyond two pieces, format! is clearer: it borrows all its arguments and builds a new String:

fn main() {
    let name = String::from("Ada");
    let year = 1843;
    let msg = format!("{name} published in {year}; {name} was 27");
    println!("{msg}");
    let parts = ["a", "b", "c"];
    println!("{}", parts.join("-"));
    println!("{}", "ab".repeat(3));
}
Ada published in 1843; Ada was 27
a-b-c
ababab

join on a slice of strings inserts a separator between elements, which neatly solves the "spaces between items but not after the last" problem from module 3.

Comparing strings

== compares contents and works across the two types: String == &str, &str == String. Ordering comparisons (<) compare byte by byte, which for ASCII is alphabetical with uppercase before lowercase. eq_ignore_ascii_case compares ignoring ASCII case.

fn main() {
    let s = String::from("apple");
    println!("{} {} {}", s == "apple", "apple" < "banana", "Zebra" < "apple");
    println!("{}", "HeLLo".eq_ignore_ascii_case("hello"));
}
true true true
true

Common mistakes

&String parameters: work, but refuse literals. Use &str.

Expecting + to leave the left side usable: it moves it. Use format! when you need to keep both.

Returning &str to new text: a function that builds text must return String (module 5's dangling references). fn make() -> &str { &format!("x") } fails with E0515.

Mismatched types: passing String where &str is expected gives E0308 with the help "consider borrowing here: &name". Passing &str where String is expected needs .to_string().

Recap

Going further: look up String::from_utf8 and str::as_bytes in the docs, the bridge between strings and raw bytes that the next lesson explores.

Exercises

Exercise 1 · quiz

Which type is it?

Which of these expressions have type String (as opposed to &str)? Select all that apply.

Answer
  • String::from("hello")

  • "hello".to_string()

  • format!("{}!", "hi")

String is the owned, growable type: you get one from String::from, to_string, format!, to_uppercase and similar functions that create new text. A literal is a &str pointing into the program's static data, and slicing or as_str() produce &str views into an existing String's buffer.

Exercise 2 · write code

Build a String

Write fn initials(full_name: &str) -> String that returns the first character of each word followed by a dot, with no spaces: "Grace Brewster Hopper" becomes "G.B.H.". Use split_whitespace() to get the words, word.chars().next() to get a word's first character (it returns an Option; unwrap it), and push / push to build the result.

The given main reads a line from stdin and prints the initials.

File: main.rs

Starting point

use std::io;

fn initials(full_name: &str) -> String {
    String::new()
}

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    println!("{}", initials(&line));
}

Example

input

Grace Brewster Hopper

expected output

G.B.H.

Example

input

  Ada   Lovelace

expected output

A.L.
Hint 1

Start with let mut out = String::new(); and return out at the end.

Hint 2

out.push(c) appends one char; out.push('.') the dot.

Reference solution
use std::io;

fn initials(full_name: &str) -> String {
    let mut out = String::new();
    for word in full_name.split_whitespace() {
        out.push(word.chars().next().unwrap());
        out.push('.');
    }
    out
}

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    println!("{}", initials(&line));
}

Exercise 3 · write code

Concatenation errors

This program tries to join strings with + and fails to compile in two places. Fix it so it prints:

Hello, Ada!
Ada Ada

Keep using + for the first line; use format! for the second.

File: main.rs

Starting point

fn main() {
    let name = String::from("Ada");
    let greeting = "Hello, " + name + "!";
    println!("{greeting}");
    let twice = name + " " + name;
    println!("{twice}");
}

Example

expected output

Hello, Ada!
Ada Ada
Hint 1

+ needs an owned String on the left and a &str on the right: String::from("Hello, ") + &name + "!".

Hint 2

format!("{name} {name}") borrows name and never moves it.

Reference solution
fn main() {
    let name = String::from("Ada");
    let greeting = String::from("Hello, ") + &name + "!";
    println!("{greeting}");
    let twice = format!("{name} {name}");
    println!("{twice}");
}