What is the purpose of AsRef?

I was checking the phone number crate and I came across with the function signature

pub fn is_viable<S: AsRef<str>>(string: S) -> bool

Here what is the point of AsRef how is it different from using &str ?

2 Likes

It's a slight convenience for the caller of the function. You can pass either a normal &str, or anything else that meets the AsRef<str> trait bound. Most notably, String meets that bound, so this allows you to call the same function on either a &str or a String.

(If the signature was just pub fun is_viable(string: &str) -> bool, and all you had to call that function on was a String, you would need to call .as_ref() on your string first in order to make it a &str you could pass to is_viable. So it's nicer if is_viable does that for you.)

2 Likes

that sounds cool. and as a writer how do you handle the param string of type S? can I use it as a drop in replacement for the place I use &str?

The only thing you know about S here is that it is a type that implements AsRef<str>. (Because of the S: AsRef<str> in the type signature.)

So the one and only thing you're allowed to do with a parameter of type S in this case it is to use methods from the AsRef trait. AsRef has exactly one method, .as_ref(). So the only thing the implementer of is_viable can do is use .as_ref() on that parameter, getting (in this case) a value of type &str.

So if you're implementing a function, and you intend for your function to take a &str argument, you might instead have it take a generic argument S: AsRef<str>, and then call .as_ref() on that argument to get the &str you needed. As the implementer of the function, either way you end up with a supplied &str, but the generic version makes it slightly more ergonomic for callers who have not a &str but, say, a String. (Because you're the one converting it for them, and they don't have to think about the conversion at all.)

5 Likes

I see thank you :pray: :slight_smile:

Using AsRef<str> most often than not will just allow the caller to avoid having to type a leading & in front of their expression, since most stuff that is AsRef<str> is also Deref<Target = str> (e.g., coercing a &String to a &str).

This, then has a few minor drawbacks to be aware of:

  • The function can take ownership of an input String when what it really needed was just a borrow; this may lead a beginner into .clone-ing needlessly.

    For instance,

    fn print_str<S> (s: S)
    where
        S : AsRef<str>,
    {
        let s: &str = s.as_ref();
        println!("{}", s);
    }
    
    fn main ()
    {
        let s = String::from("Hello, World!");
        print_str(s);
        print_str(s);
    }
    

    errors with

    error[E0382]: use of moved value: `s`
      --> src/main.rs:11:15
       |
    9  |     let s = String::from("Hello, World!");
       |         - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
    10 |     print_str(s);
       |               - value moved here
    11 |     print_str(s);
       |               ^ value used here after move
    

    and a beginner may be tempted to fix this with:

    fn print_str<S> (s: S)
    where
        S : AsRef<str>,
    {
        let s: &str = s.as_ref();
        println!("{}", s);
    }
    
    fn main ()
    {
        let s = String::from("Hello, World!");
        print_str(s.clone());
        print_str(s);
    }
    
  • Given how a generic function works (a generic function is actually a function factory; for every different type <S> the generic is called with, Rust will monomorphise the generic into a new concrete function (print_str::<String>, print_str::<&str>, print_str::<Rc<str>>), i.e., compile a new version of the function), this leads to a little bit longer compile times and potentially bigger binaries. This can be fixed with the following pattern, (done, for instance, by ::momo's #[momo] attribute macro):

    fn foo<S> (s: S)
    where
        S : AsRef<str>,
    {
        let s: &str = s.as_ref();
        /*
         *  VERY
         *  LONG
         *  FUNCTION
         *  '
         *  S
         *  BODY
         */
    }
    

    becomes

    #[inline]
    fn foo<S> (s: S) // same public prototype
    where
        S : AsRef<str>,
    {
        fn foo_common (s: &str)
        {    
            /*
             *  VERY
             *  LONG
             *  FUNCTION
             *  '
             *  S
             *  BODY
             */
        }
    
        foo_common(s.as_ref())
    }
    

This being said, the trait is indeed useful: imagine that you want to take a sequence of strings (to, let's say, print them all):

  • How do you express the "sequence-ness"?
    Vec<..> is too restrictive (in the same way as taking a String instead of a &str is), so you can take a impl AsRef<[..]>, in the same vein as you were taking a impl AsRef<str>. As I said, there is little benefit in this case, where taking a &[..] is just as fine.

  • How do you express the "string-ness"?
    String is too restrictive (e.g., it wouldn't accept &["Hello", "World!"]), but it turns out that so is &str! Indeed, taking a &[&str] does not accept a
    &[String::from("Hello"), String::from("World!")]
    .

    This is where things like AsRef really do shine: by taking a &[impl AsRef<str>], we manage to have a function accepting both cases!

    fn print_strs (strs: &[impl AsRef<str>])
    {
        println!("{}", ::itertools::Itertools::format(
            strs.iter().map(AsRef::as_ref),
            ", ",
        ));
    }
    
    fn main ()
    {
        print_strs(&[
            "Hello",
            "World!",
        ]);
        print_strs(&[
            "Hello".to_string(),
            "World!".to_string(),
        ]);
    }
    
(and for the record, here is the most general input this kind of function can take)
11 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.