S1.0 What This Teaches
&strvsString: what each is and why both exist- Constructing and concatenating
Stringvalues - Common query and transformation methods
- Splitting, iterating over characters, and parsing
S1.1 Two String Types
&str (string slice) is a reference to a sequence of UTF-8 bytes stored
somewhere else - usually in the program's read-only binary or in a String
on the heap. It is borrowed: you cannot grow it or own it.
String is an owned, heap-allocated, growable buffer of UTF-8 bytes. You
can append to it, modify it, and pass ownership of it.
let s: &str = "hello"; // lives in read-only memory; borrowed
let t: String = String::from(s); // heap-allocated copy; owned
&str
- it accepts both &str literals and &String via deref
coercion. Parameters that need to own or grow the string take String.
S1.2 Constructing a String
let a = String::from("Rust");
let b = "world".to_string();
let c = format!("{a} {b}"); // format! never moves its arguments
println!("{c}"); // Rust world
format! is the most flexible constructor - it composes values of any type
without allocating intermediate strings, and does not move any of its arguments.
S1.3 Concatenation
+ operator appends a &str to a String.
It moves the left operand, so the original binding becomes invalid afterward:
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved; s2 is borrowed
// s1 is no longer usable here
println!("{s3}"); // Hello, world!
format! - it does not move any of
its arguments and produces a fresh String.
S1.4 Query Methods
let msg = String::from(" Hello, Rust! ");
println!("{}", msg.len()); // 16 (byte count, not char count)
println!("{}", msg.contains("Rust")); // true
println!("{}", msg.trim().starts_with("Hello")); // true
println!("{}", msg.trim().ends_with("!")); // true
len() returns byte length. For ASCII strings that equals character count,
but multi-byte Unicode characters cause them to differ. Use
.chars().count() for character count.
S1.5 Transformation Methods
let msg = " Hello, Rust! ";
println!("{}", msg.trim()); // "Hello, Rust!"
println!("{}", msg.trim().to_uppercase()); // "HELLO, RUST!"
println!("{}", msg.trim().to_lowercase()); // "hello, rust!"
println!("{}", msg.trim().replace("Rust", "World")); // "Hello, World!"
S1.6 Splitting
split returns a lazy iterator of &str slices. Call
.collect() to gather them into a Vec:
let csv = "one,two,three,four";
let parts: Vec<&str> = csv.split(',').collect();
println!("{parts:?}"); // ["one", "two", "three", "four"]
split_whitespace(),
splitn(n, pat), lines().
S1.7 Characters
s[0] on a string. Instead, iterate over Unicode scalar
values with .chars():
for ch in "Rust".chars() { print!("{ch} "); }
println!();
println!("{}", "Rust".chars().count()); // 4
.chars() returns an iterator, all iterator adaptors
(filter, map, enumerate, ...) apply directly.
S1.8 Conversion and Parsing
&String coerces to &str automatically (deref
coercion) - no copy occurs, just a reference into the String's buffer:
let owned: String = String::from("owned");
let borrowed: &str = &owned; // reference into owned's buffer
println!("{borrowed}");
&str into a numeric type with .parse(). It
returns Result - use ? in a function that propagates errors,
or .unwrap() when failure cannot happen:
let n: i32 = "42".parse().unwrap();
println!("{n}"); // 42
S1.9 Example - All Together
// Strings - demonstrates String vs &str, construction, common methods, and conversion.
fn main() {
// --- String vs &str ---
println!("--- String vs &str ---");
let s: &str = "hello";
let t: String = String::from(s);
println!("&str: {s}, String: {t}");
// --- construction ---
println!("--- construction ---");
let a = String::from("Rust");
let b = "world".to_string();
let c = format!("{a} {b}");
println!("{c}");
// --- concatenation with + moves the left operand ---
println!("--- concatenation ---");
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
println!("{s3}");
// --- common query methods ---
println!("--- query methods ---");
let msg = String::from(" Hello, Rust! ");
println!("len: {}", msg.len());
println!("contains: {}", msg.contains("Rust"));
println!("starts_with: {}", msg.trim().starts_with("Hello"));
println!("ends_with: {}", msg.trim().ends_with("!"));
// --- transformation ---
println!("--- transform ---");
println!("trim: '{}'", msg.trim());
println!("to_uppercase: {}", msg.trim().to_uppercase());
println!("to_lowercase: {}", msg.trim().to_lowercase());
println!("replace: {}", msg.trim().replace("Rust", "World"));
// --- split and collect ---
println!("--- split ---");
let csv = "one,two,three,four";
let parts: Vec<&str> = csv.split(',').collect();
println!("{parts:?}");
// --- chars: iterate over Unicode scalar values ---
println!("--- chars ---");
let word = "Rust";
for ch in word.chars() { print!("{ch} "); }
println!();
println!("char count: {}", word.chars().count());
// --- deref coercion: &String -> &str ---
println!("--- conversion ---");
let owned = String::from("owned");
let borrowed: &str = &owned;
println!("borrowed: {borrowed}");
// --- parse: &str to numeric type ---
println!("--- parse ---");
let n: i32 = "42".parse().unwrap();
println!("parsed: {n}");
}
--- String vs &str ---
&str: hello, String: hello
--- construction ---
Rust world
--- concatenation ---
Hello, world!
--- query methods ---
len: 16
contains: true
starts_with: true
ends_with: true
--- transform ---
trim: 'Hello, Rust!'
to_uppercase: HELLO, RUST!
to_lowercase: hello, rust!
replace: Hello, World!
--- split ---
["one", "two", "three", "four"]
--- chars ---
R u s t
char count: 4
--- conversion ---
borrowed: owned
--- parse ---
parsed: 42
S1.10 Exercise
Exercise
- Write a function
word_count(s: &str) -> usizethat returns the number of whitespace-delimited words. Test it on a sentence. - Write a function
capitalize(s: &str) -> Stringthat returns a newStringwith the first character uppercased and the rest unchanged. Use.chars()to get the first character and&s[1..]for the remainder. - Split
"2024-07-09"on'-'and parse each part tou32. Print year, month, and day on separate lines.
S1.11 Common Mistakes
Indexing a String with []
let s = String::from("hello");
let c = s[0]; // error: String cannot be indexed by integer
.chars().nth(0) for a character, or &s[0..1]
for a byte slice (safe only for ASCII).
Treating len() as character count
println!("{}", "café".len()); // 5 (bytes)
println!("{}", "café".chars().count()); // 4 (characters)
len() counts bytes. The é character encodes to two bytes
in UTF-8, so byte count and character count differ for non-ASCII strings.
Moving a String into + unexpectedly
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2;
println!("{s1}"); // error: s1 was moved into +
format!("{s1}{s2}") to avoid moving either operand.Passing String where &str is expected (or vice versa)
fn greet(name: String) { println!("Hello, {name}"); }
greet("Alice"); // error: expected String, found &str
greet("Alice".to_string()); // ok
greet(String::from("Alice")); // ok
&str parameters in function signatures - they accept both
string literals and &String without conversion.
S1.12 Key Terms
| Term | Meaning |
|---|---|
| &str | Borrowed string slice; reference to UTF-8 bytes stored elsewhere; cannot be grown |
| String | Owned, heap-allocated, growable UTF-8 string buffer |
| deref coercion | Automatic conversion of &String to &str by the compiler |
| format! | Macro that builds a String from a format template without moving arguments |
| .chars() | Iterator over Unicode scalar values (char), not raw bytes |
| .len() | Returns byte length of the string, which may differ from character count for Unicode |
| .parse() | Converts a &str to another type; returns Result |
| .split() | Returns a lazy iterator of &str slices divided by a pattern |
| .trim() | Returns a &str with leading and trailing whitespace removed |
| .replace() | Returns a new String with all occurrences of a pattern substituted |