Skip to content

← Generics and Traits step 11 of 24

Medium Primitives

AsRef, Borrow, and signatures your callers can actually use

Here is a function that works and is still wrong:

pub fn total_len(items: &Vec<String>) -> usize

Two things are wrong with it, and clippy tells you about one:

error: writing `&Vec` instead of `&[_]` involves a new object where a slice
       will do [clippy::ptr_arg]

ptr_arg is default-on and it teaches the whole “take the borrowed form” lesson by itself. &Vec<T> accepts only a Vec; &[T] accepts a Vec, an array, a slice of either, and a sub-range of any of them. You lose nothing and gain every caller. The same rule gives you &str over &String.

The second problem ptr_arg cannot help with: the element type is nailed down to String. A caller holding a Vec<&str> is out of luck. That is what AsRef is for.

AsRef<str>

pub fn total_len<S: AsRef<str>>(items: &[S]) -> usize

Read the bound as: “S is some type from which I can cheaply borrow a &str“. String implements it. &str implements it. Box<str> implements it. Your own newtype can implement it in three lines. One signature, all of them.

Two facts about AsRef that beginners get wrong:

  • It is not transitive. A: AsRef<B> and B: AsRef<C> does not give you A: AsRef<C>. There is no blanket chain, and people expect one.
  • It is a cheap reference-to-reference conversion by contract. Not a parse, not an allocation. If your impl allocates, callers will be surprised.

Your task

pub fn total_len<S: AsRef<str>>(items: &[S]) -> usize
pub fn run(words: Vec<String>) -> Vec<usize>

total_len sums the byte lengths (str::len, not character count) of its items. run calls it twice on the same data — once over the Vec<String> it was given, and once over a Vec<&str> borrowed from it — and returns both answers. They must be identical. That identity is the whole demonstration: one generic function serving two different element types.

Note the byte-length detail in the tests. "héllo" is five characters and six bytes; str::len reports bytes.

Both directions are policed

Learners who take the “use references” advice too enthusiastically meet the other side of the gate:

error: this expression creates a reference which is immediately dereferenced
       by the compiler [clippy::needless_borrow]

and its relative clippy::needless_borrows_for_generic_args, which fires when you write &x for a generic parameter that would have taken x happily. So the rule is not “always add &“ — it is “borrow at the boundary, and let coercion do the rest”.

AsRef vs Borrow — the distinction worth knowing

Borrow<T> has the same shape as AsRef<T> and one extra clause in its contract:

Eq, Ord and Hash on the borrowed form must be equivalent to those on the owned form.

That extra promise is what makes heterogeneous map lookup sound. This works:

let map: HashMap<String, i32> = ...;
map.get("key")            // &str probe against String keys

because HashMap::get is bounded on Borrow, and String: Borrow<str> guarantees that hashing the &str gives the same value as hashing the String. If get were bounded on AsRef, a type whose AsRef<str> returned something unrelated to its own hash would silently break the map.

And here is the part that matters: the compiler cannot verify that promise. It is a semantic obligation on the implementor, exactly like Hash/Eq agreement. Choose Borrow when your conversion preserves identity; choose AsRef when it is merely a convenient view.

(One deny-level lint guards a specific instance of this: clippy::impl_hash_borrow_with_str_and_bytes, which rejects a type that implements both Borrow<str> and Borrow<[u8]> alongside Hash — since str and [u8] hash differently, one of the two lookups would be broken.)

Remember the grade is compile + tests + clippy -D warnings.