Skip to content

← Ownership II: Borrowing and the Borrow Checker step 10 of 24

Easy Primitives

&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 an e followed by a combining accent (U+0065 U+0301) are different sequences, so they share only what their chars 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).

  • &String is a reference to that three-word String value. Reading the text goes through two pointers: first to the String, then to its bytes.
  • &str is 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.