Chapter 2

Strings, &str, and chars

👋 Anyone can read and edit this exercise. Sign up to save your progress.

An i32 walks up to a String and asks for its number. The String replies: "Sorry, you're not my type."

Many other languages have a mushy concept of a "string" that can be anything from a null-terminated byte array to a UTF-16 buffer to something else entirely (I'm looking at you, Perl). Rust splits "string" across distinct types:

Before we go further, let's take a moment to briefly introduce two words that I will use a lot going forward:

This ownership model is why Rust has two string types in the first place: it tracks who owns each piece of data. The brief version is that every value has one owner, the value is dropped (deleted) when that owner goes out of scope, and you can borrow a value without owning it. The useful mental model is "one owner, many borrows."

The split between &str and String is what makes Rust strings both fast and safe. A function that just reads text takes &str and Rust avoids any unnecessary copies or allocations. A function that produces new text returns String and returns ownership to the caller, who can then decide what to do with it.

You'll see this pattern again and again:

fn shout(text: &str) -> String {
    text.to_uppercase()
}

let s = String::from("hello");
let louder = shout(&s); // &String coerces to &str

There's one common gotcha: If you call .len() on a string, it returns the number of bytes, not the number of characters in that string. Rust uses UTF-8 for strings, which means a single visible character can take more than one byte. So if you rather need the number of "human-readable" characters in a string, use s.chars().count() instead.

.chars() lets you walk through the char values in a string, one at a time. The returned value is an iterator over those characters. Iterator provides methods such as .next(), .count(), and .any(...) for consuming or inspecting its values. We'll get to iterators in more detail later.

Building a String with format!

A convenient way to assemble a new String is the format! macro. It works like println!, except instead of printing, it returns the formatted text:

let name = "Alice";
let greeting: String = format!("Hello, {name}!");

In the format string, {name} is a captured identifier. Rust pulls the variable from the surrounding scope. Sometimes you still see format!("Hello, {}!", name) instead, which is the pre-2021 version, but both forms still work. The exclamation mark (!) means format! is a macro rather than a regular function call.

Consuming an iterator

The simplest way to consume an iterator is a for loop:

for c in "hello".chars() {
    println!("{c}");
}

You can read it as "for each c produced on the right, run the body once." In this case, for each character in the string "hello", do something with it. The loop variable is a fresh binding scoped to each iteration. Ranges, arrays, and collections can also go on the right-hand side because each can produce an iterator.

Where to look things up

  • std::fmt contains everything the formatting macros can do (padding, precision, hex, debug output…).
  • str: the inventory of operations available on any &str.

A welcome message

Time to put &str and String together. Implement format_welcome_message so it returns the string "Welcome, {name}!".

The signature already tells you what to do:

fn format_welcome_message(name: &str) -> String

This means: you're handed a borrowed &str to read from, and you produce a fresh, owned String to hand back.

Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Build and return the welcome message for `name`.
    fn format_welcome_message(name: &str) -> String {
        format!("Welcome, {name}!")
    }
    
    // Tests live right next to the code they exercise. Don't worry about the syntax yet.
    
    #[test]
    fn test_format_welcome_message() {
        assert_eq!(format_welcome_message("Alice"), "Welcome, Alice!");
        assert_eq!(format_welcome_message("Bob"), "Welcome, Bob!");
    }
    

    Counting characters

    In many languages, asking for the "length" of a string gives you back the number of characters. In Rust, str::len returns the number of bytes in the underlying UTF-8 buffer. For "hello" the byte count and char count both happen to be 5, but "café" is 5 bytes and 4 chars. That's because the é is two bytes in UTF-8.

    To get the actual character count, always use chars().

    Useful from the standard library

    • str::chars iterates over the chars of a string. The starting point for almost any character-level work.
    • Iterator::count consumes an iterator and returns how many items it produced.
    • str::len is byte length, not character count. Useful, but not what you want here.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Reveal the full solution Spoiler: the complete answer
      /// Counts how many characters are in `text`.
      ///
      /// Watch out: `text.len()` returns the number of bytes, not characters.
      /// For "hello" those happen to be the same, but for "café" they aren't.
      /// See: <https://doc.rust-lang.org/std/primitive.str.html#method.chars>
      fn count_chars(text: &str) -> usize {
          text.chars().count()
      }
      
      #[test]
      fn test_count_chars() {
          assert_eq!(count_chars("hello"), 5);
          assert_eq!(count_chars("rust"), 4);
          assert_eq!(count_chars(""), 0);
      }
      

      Borrow in, own out

      Here you borrow text to read it, then return a new String that the caller can own (i.e. use however they like). The signature is very typical: you often get a &str in and an owned String out. This is common when a function reads existing string and does some processing to create a new string.

      Useful from the standard library

      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        /// Takes a borrowed `&str` and returns an owned, uppercased `String`.
        ///
        /// Notice the signature: borrow on the way in, own on the way out. That's
        /// the pattern the table in the chapter intro is hinting at, and you'll
        /// see it everywhere in real Rust code.
        /// See: <https://doc.rust-lang.org/std/primitive.str.html#method.to_uppercase>
        fn shout(text: &str) -> String {
            text.to_uppercase()
        }
        
        #[test]
        fn test_shout() {
            assert_eq!(shout("hello"), "HELLO");
            assert_eq!(shout("Rust"), "RUST");
            assert_eq!(shout(""), "");
        }
        

        Iterating over characters

        Strings aren't directly indexable in Rust, because UTF-8 characters have varying widths, but you can walk through their chars. A for c in text.chars() loop works. When the question is "does at least one character match?", you can use any(), which stops as soon as it finds one.

        Useful from the standard library

        • Iterator::any returns true if any item in the iterator matches a predicate. Stops at the first match, so it's cheap.
        • char::is_uppercase and char::is_ascii_uppercase classify a single character. The Unicode-aware version is the safer default; the ASCII version is faster when you know the input is ASCII.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Reveal the full solution Spoiler: the complete answer
          /// Returns true if `text` contains at least one ASCII uppercase letter.
          ///
          /// `for c in text.chars()` lets you inspect each character; the iterator
          /// methods (`any`, `find`, ...) usually express this kind of "is there
          /// at least one ..." check more directly.
          /// See: <https://doc.rust-lang.org/std/primitive.char.html#method.is_ascii_uppercase>
          fn has_uppercase(text: &str) -> bool {
              text.chars().any(|c| c.is_ascii_uppercase())
          }
          
          #[test]
          fn test_has_uppercase() {
              assert!(has_uppercase("Hello"));
              assert!(has_uppercase("rustY"));
              assert!(!has_uppercase("hello"));
              assert!(!has_uppercase(""));
          }
          

          Wrapping up strings and chars

          You worked with all three string types: counted UTF-8 characters correctly, took a &str and produced a fresh String, and walked a string character by character.

          What we learned

          • &str is a borrowed view into UTF-8 text; String is an owned, growable buffer; char is one Unicode scalar value. Functions that read take a &str, functions that produce return a String.
          • str::len is for byte length, NOT character count. Use s.chars().count() when you mean characters.
          • str::chars() returns an iterator. Anything that takes an iterator works on it: for c in s.chars(), s.chars().any(...), s.chars().count(), and so on.
          • Case conversion (to_uppercase, to_lowercase) returns a new String. The original string does not get mutated.
          • The is_ascii_* family of functions is fast when you know the input is ASCII, but char::is_uppercase is the Unicode-aware version and is the safer default.
          Next chapter 3Moves and Copy