ExamplesBy LevelBy TopicLearning Paths
471 Fundamental

String vs &str

Functional Programming

Tutorial

The Problem

Many languages have a single string type. Rust distinguishes between ownership and borrowing at the type level: String owns heap memory and can grow; &str is a fat pointer (address + length) into any UTF-8 bytes — a string literal in the binary, a slice of a String, or a network buffer. This design enables functions to accept both "literals" and String values without copying, a guarantee enforced at compile time rather than at runtime.

🎯 Learning Outcomes

  • • Understand String as Vec<u8> with a UTF-8 invariant versus &str as a borrowed view
  • • Write functions that accept &str to work with both String and string literals
  • • Use String::from / .to_string() / format! to create owned strings
  • • Slice a String to obtain a &str with the same lifetime
  • • Implement first_word using byte-level find to return a slice of the input
  • Code Example

    #![allow(clippy::all)]
    // 471. String vs &str: ownership semantics
    fn greet(name: &str) {
        println!("Hello, {}!", name);
    }
    fn make_greeting(name: &str) -> String {
        format!("Hello, {}!", name)
    }
    fn first_word(s: &str) -> &str {
        &s[..s.find(' ').unwrap_or(s.len())]
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_greet() {
            let s = String::from("test");
            let g = make_greeting(&s);
            assert_eq!(g, "Hello, test!");
        }
        #[test]
        fn test_literal() {
            assert_eq!(make_greeting("hi"), "Hello, hi!");
        }
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    }

    Key Differences

  • Ownership: Rust's String is uniquely owned and freed when it goes out of scope; OCaml strings are garbage-collected — ownership is irrelevant.
  • Zero-copy slices: Rust &str slices point into existing memory; OCaml String.sub always copies.
  • Mutability: Rust String is mutable via push_str/push; OCaml string is immutable — mutation requires Bytes.t.
  • Deref coercion: &String coerces to &str automatically; OCaml has no such coercion — you pass string values directly.
  • OCaml Approach

    OCaml's string is an immutable byte sequence; Bytes.t is the mutable counterpart. There is no ownership distinction at the type level:

    let greet name = Printf.printf "Hello, %s!\n" name
    let make_greeting name = "Hello, " ^ name ^ "!"
    let first_word s =
      match String.index_opt s ' ' with
      | Some i -> String.sub s 0 i
      | None   -> s
    

    String.sub always allocates a new string; there is no zero-copy slice type in the standard library (Bigstring from core provides views for I/O).

    Full Source

    #![allow(clippy::all)]
    // 471. String vs &str: ownership semantics
    fn greet(name: &str) {
        println!("Hello, {}!", name);
    }
    fn make_greeting(name: &str) -> String {
        format!("Hello, {}!", name)
    }
    fn first_word(s: &str) -> &str {
        &s[..s.find(' ').unwrap_or(s.len())]
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_greet() {
            let s = String::from("test");
            let g = make_greeting(&s);
            assert_eq!(g, "Hello, test!");
        }
        #[test]
        fn test_literal() {
            assert_eq!(make_greeting("hi"), "Hello, hi!");
        }
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_greet() {
            let s = String::from("test");
            let g = make_greeting(&s);
            assert_eq!(g, "Hello, test!");
        }
        #[test]
        fn test_literal() {
            assert_eq!(make_greeting("hi"), "Hello, hi!");
        }
        #[test]
        fn test_first_word() {
            assert_eq!(first_word("hello world"), "hello");
            assert_eq!(first_word("single"), "single");
        }
    }

    Exercises

  • Longest word: Write longest_word(s: &str) -> &str that returns the longest whitespace-delimited word as a slice without allocating.
  • String conversion benchmark: Use criterion to measure the cost of .to_string() vs. String::from() vs. format!("{}", s) for a 100-byte input.
  • **Accept impl AsRef<str>**: Rewrite greet to accept impl AsRef<str> and verify it works with String, &str, and Cow<str>.
  • Open Source Repos