Step 2 of 6
&str: the type for text parameters
Now you can see what string literals really are. A literal like "hello" is stored in the program's binary, and the value you get is a slice pointing at it: that's why its type is &str, and why it's read-only. (Its full type is &'static str: a borrow that's valid for the whole run of the program. More on that in the lifetimes module.)
So text comes in three shapes: an owned String, a slice of a String, and a literal. A function that takes &String only accepts the first. A function that takes &str accepts all three, because Rust automatically turns a &String into a &str covering the whole text. This automatic conversion is called deref coercion.
fn shout(text: &str) -> String {
text.to_uppercase()
}
fn main() {
let owned = String::from("ferris wins");
println!("{}", shout(&owned)); // &String becomes &str automatically
println!("{}", shout(&owned[7..])); // a slice of it
println!("{}", shout("gg")); // a literal
}
FERRIS WINS
WINS
GG
With a &String parameter, the literal doesn't fit:
fn shout(text: &String) -> String {
text.to_uppercase()
}
fn main() {
println!("{}", shout("gg"));
}
error[E0308]: mismatched types
--> src/main.rs:6:26
|
6 | println!("{}", shout("gg"));
| ----- ^^^^ expected `&String`, found `&str`
Prefer &str over &String
Workplace note: for a parameter that only reads text, write &str. It accepts more kinds of argument and loses nothing: every method that reads text (len, chars, contains, to_uppercase, trim, ...) is a str method, which String just makes available. Clippy flags &String parameters with its ptr_arg lint ("writing &String instead of &str involves a new object where a slice will do"). Take an owned String only when the function keeps the text, for example to store it in a vector.
String |
&str |
|
|---|---|---|
| Owns its text? | yes | no, borrows it |
| Can grow or change? | yes (if mut) |
no |
| Where the text is | its own heap block | anywhere: a String's heap block, or the program's binary |
| Size of the value | 24 bytes (pointer, length, capacity) | 16 bytes (pointer, length) |
| Get the other one | &s, &s[..] or s.as_str() |
s.to_string() or String::from(s) |
Methods that return slices
Several str methods hand you slices of the original text instead of new strings. trim() returns the part without the spaces at both ends, as a &str pointing into the original:
fn main() {
let input = String::from(" add milk ");
let cleaned: &str = input.trim(); // a slice of input: nothing copied
println!("[{}] {} bytes", input, input.len());
println!("[{}] {} bytes", cleaned, cleaned.len());
}
[ add milk ] 14 bytes
[add milk] 8 bytes
split_whitespace(), which you used for reading input, works the same way: each word it gives you is a &str slice of the original line. No copies.
Common mistakes
&Stringparameters. They work, but they force every caller to have aString. Use&str.- Trying to change a
&str. It's a read-only view. To build or change text, use aString. - Returning a
&strthat points into aStringcreated inside the function (E0515 from the borrowing module). Return theString.
Your task
The two functions take &String, so the calls with a slice and with a literal don't compile. Change both parameters to &str (don't change main). The program should print:
6 words
first part: 2 words
literal: 2 words
flagged: true
title flagged: false
Challenge 2
Write initials, which returns the first letter of each word, in capitals: "Ada Lovelace" gives "AL", and "grace brewster hopper" gives "GBH". Take a &str so it works with literals and Strings alike. Loop over full_name.split_whitespace() and take each word's first letter with a slice (&word[..1]: the names are plain ASCII). Then uppercase the result.
Challenge 3
Write censor. It returns a copy of text in which every occurrence of banned is replaced by as many * as banned has characters: censor("well darn it", "darn") returns "well **** it". Both parameters are &str, so it works on Strings, slices and literals. (text.replace(from, to) returns a new String, as you saw in the strings intro module.)