We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 10 of 24
&str not &String, &[T] not &Vec<T>: deref coercion and ptr_arg
pub fn longest_common_prefix(words: &[String]) -> String
Return the longest string that is a prefix of every word in words.
- Words are compared one Rust
char(Unicode scalar value) at a time, not one byte at a time. The result is therefore always valid text. - The comparison is exact: it is case-sensitive and does no Unicode
normalization. A precomposed
é(U+00E9) and anefollowed by a combining accent (U+0065 U+0301) are different sequences, so they share only what theirchars share. - An empty list gives
"". A list containing an empty word gives"". A list with one word gives that word. - The result is a prefix of every word, so it is also a prefix of the first
word. It may end in the middle of what a reader sees as one letter:
"e"plus U+0301 and"e"plus U+0300 share only"e".
[] -> ""
["flower","flow","flight"] -> "fl"
["dog","cat"] -> ""
["abc"] -> "abc"
["héllo","héro"] -> "hé"
In the last example, é is one char but two bytes in UTF-8, so the answer
"hé" is two chars and three bytes. Comparing bytes is not wrong in
itself, but it can stop in the middle of a character: é and è start with
the same byte and differ in the second. Slicing a &str at a position that is
not a character boundary panics. Comparing chars avoids that case.
What you need to change
The starter code already returns the right answer for every test. It does
not pass, because a submission is graded on three things: it must compile,
pass the tests, and pass cargo clippy -- -D warnings. Clippy reports this
for the helper function (once for each of its two parameters):
error: writing `&String` instead of `&str` involves a new object where a slice will do
--> src/lib.rs:1:25
|
1 | fn shared_prefix_len(a: &String, b: &String) -> usize {
| ^^^^^^^
|
= note: `-D clippy::ptr-arg` implied by `-D warnings`
clippy::ptr_arg is a warn-by-default lint. -D warnings turns every
warning into an error, so the warning fails the grade. The fix is to change
the helper's parameter types; nothing else in the code needs to change.
Two ways to borrow text
A String is an owned, growable text buffer. The String value itself holds
three machine words: a pointer to its bytes, the length, and the capacity.
When the capacity is nonzero, the bytes live in a heap allocation the
String owns (String::new() starts with capacity zero and does not
allocate).
&Stringis a reference to that three-wordStringvalue. Reading the text goes through two pointers: first to theString, then to its bytes.&stris a string slice: a pointer straight to some UTF-8 bytes plus a length. It does not say who owns the bytes or how they were stored.
&String ──▶ [ ptr | len | cap ] ──▶ "hello"
&str = [ ptr | len ] ─────────▶ "hello"
The extra pointer is usually a small cost. The larger difference is which
callers can use the function. Many kinds of text are available as &str
but not as a String:
fn takes_string(s: &String) -> usize { s.len() }
fn takes_str(s: &str) -> usize { s.len() }
let owned = String::from("hello, world");
takes_str("literal"); // a string literal has type &'static str
takes_str(&owned); // &String converts to &str (explained below)
takes_str(&owned[7..12]); // a slice of part of the String, no copy
takes_string(&owned); // fine
// takes_string("literal"); // error[E0308]: expected `&String`, found `&str`
The range in &owned[7..12] is in bytes, and slicing panics if either end
is not on a character boundary; here every character is one byte.
A caller who has only a &str must build a new String to call
takes_string, for example with "literal".to_string(). For nonempty text
that allocates and copies, even though the function only reads the text.
The same reasoning applies to &Vec<T> versus the slice &[T], and to
&PathBuf versus &Path. ptr_arg reports all three.
Deref coercion: why existing calls keep compiling
After you change the helper to take &str, look at its call site:
n = n.min(shared_prefix_len(first, w)); // `first` and `w` have type &String
It does not need to change. split_first on a &[String] and the for loop
over rest both give &String values, and the compiler converts them.
This conversion is deref coercion. String implements
Deref<Target = str>. When a function argument has type &T, the parameter
expects &U, and T: Deref<Target = U>, the compiler inserts the
dereference for you. The decision is made at compile time; at run time it is
just reading the slice's pointer and length out of the String. In the same
way Vec<T> implements Deref<Target = [T]>, so &Vec<i64> coerces to
&[i64], and through DerefMut, &mut Vec<i64> coerces to &mut [i64].
Method calls use a related rule. When you write s.trim() on a String, the
compiler looks for trim on String, does not find it, and then tries the
dereferenced type str, where it is defined. That is why many str and
slice methods can be called directly on String and Vec values.
So for ordinary calls, widening a parameter from &String to &str keeps
every existing call compiling and also accepts callers who only have a
&str. Coercion applies to arguments at a call site, not to a function used
as a value. If some code passes the function by name, as in
strings.iter().map(takes_str) where the items are &String, changing the
parameter type makes that code fail with E0631 (type mismatch in function
arguments); a closure such as |s| takes_str(s) fixes it. That matters
when you change a public function in a library; it does not matter for the
private helper here.
When &String is accepted
ptr_arg looks at how the parameter is used. It does not report a &String
parameter whose body needs the String type itself, for example by calling
capacity() or by passing the reference on to another function that takes
&String. Like any lint, it is a heuristic, so read its suggestion and check
that it fits your code rather than applying it automatically.
A function that needs to change or keep the text needs a different type
altogether: &mut String to append to the caller's buffer, or String to
take ownership. In this problem the helper only reads characters, so &str
is the right type.
About the outer signature
longest_common_prefix(words: &[String]) borrows a slice whose elements are
owned Strings. That is already the slice form: the caller can pass a
&Vec<String>, which coerces to &[String], or part of a vector. Writing
the parameter as &Vec<String> is what ptr_arg would report.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.