In PHP
There is one string type. .= appends. strlen counts bytes. mb_strlen counts characters. $text[0] reads the first byte, which for héllo happens to be h.
<?php
$text = "héllo";
$text .= " world";
echo $text, "\n";
echo strlen($text), " ", mb_strlen($text), "\n";
echo $text[0], "\n";
$name = "Ada";
echo "Hello, $name\n";
strlen prints 12. mb_strlen prints 11.
In Rust
String owns the text and can grow. &str is a borrowed view: a literal, or the type of a function parameter such as name: &str. format! builds a new String. push_str appends. Ownership and borrowing are chapter 3. This lesson only names the two types.
.len() counts bytes, like strlen. .chars() walks Unicode scalar values, like mb_strlen. text[0] does not compile. A string index is a range of bytes:
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:25
|
3 | println!("{}", text[0]);
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
&text[0..2] on héllo compiles, then panics, because those two bytes split é:
thread 'main' panicked at src/main.rs:3:25:
byte index 2 is not a char boundary; it is inside 'é' (bytes 1..3) of `héllo`
.chars().next() is the first character, as Option.
fn greet(name: &str) -> String {
format!("Hello, {name}")
}
fn main() {
let literal: &str = "héllo";
let mut owned = String::from(literal);
owned.push_str(" world");
println!("{owned}");
let message = greet("Ada");
println!("{message}");
let bytes = owned.len();
let chars = owned.chars().count();
println!("{bytes} {chars}");
match owned.chars().next() {
Some(first) => println!("{first}"),
None => println!("empty"),
}
let raw = " héllo world ";
let trimmed = raw.trim();
let parts: Vec<&str> = trimmed.split(' ').collect();
let joined = parts.join("-");
let has_world = trimmed.contains("world");
let swapped = trimmed.replace("world", "Rust");
let upper = trimmed.to_uppercase();
let lower = trimmed.to_lowercase();
let starts = trimmed.starts_with("héllo");
let parsed: Result<i32, _> = "42".parse();
let left = String::from("mug");
let right = String::from(" bowl");
let joined_plus = left + &right;
println!("{trimmed}");
println!("{parts:?}");
println!("{joined}");
println!("{has_world} {starts}");
println!("{swapped}");
println!("{upper}");
println!("{lower}");
println!("{parsed:?}");
println!("{joined_plus}");
}
owned prints héllo world. greet prints Hello, Ada. The lengths are 12 and 11. The first character is h. collect gathers the pieces into the Vec named in the type. trim leaves héllo world, split then join prints héllo-world, replace prints héllo Rust, to_uppercase prints HÉLLO WORLD, to_lowercase prints héllo world, parse prints Ok(42), and + prints mug bowl. After left + &right, left has moved: you cannot use it again. Prefer format! or push_str when you still need both sides.
Watch for:
&text[0..2]is a byte range. Onhélloit panics insideé. Walk characters with.chars().
Watch for:
intval("abc")returns0."abc".parse::<i32>()returnsErr, so the failure can't be missed.
Idiomatic Rust: Take
&strwhen a function only needs to read text. ReturnStringfromformat!when the caller should own the result..chars()when you mean characters.
Side by side
| PHP | Rust | |
|---|---|---|
| Owned text | string |
String |
| A view of text | substr copies |
&str |
| Build a line | "Hello, $name" |
format!("Hello, {name}") |
| Bytes | strlen |
.len() |
| Characters | mb_strlen |
.chars().count() |
| First unit | $text[0] is a byte |
text[0] does not compile. .chars().next() |
| Trim | trim |
trim() |
| Split | explode |
split, then collect into Vec<&str> |
| Join | implode |
join |
| Contains | str_contains |
contains |
| Replace | str_replace |
replace |
| Upper case | strtoupper |
to_uppercase |
| Lower case | strtolower |
to_lowercase |
| Concatenate | . |
s1 + &s2 (moves s1), or format! / push_str |
| Prefix | str_starts_with |
starts_with |
| Integer | intval |
parse::<i32>() |
Try it
Open src/main.rs in the Playground. Confirm 12 11 for héllo world. Change "héllo" to "hello" and run it again: the lengths become 11 11. Put héllo back, then try println!("{}", &owned[0..2]); and read the char-boundary panic inside é.
Key takeaways
Stringowns growable text.&stris a borrowed view, including literals andname: &str.format!builds aString.push_strappends.s1 + &s2concatenates and movess1..len()is bytes..chars()is characters.text[0]does not compile.- A range such as
&text[0..2]is bytes, and it panics when it splits a character.
What's next: match and patterns