How to accept &str, String and &String in a single function?
Asked Answered
G

3

53

I want to write a single function, that accepts a &str, a String and a borrowed &String. I've written the following 2 functions:

fn accept_str_and_ref_string(value: &str) {
    println!("value: {}", value);
}

fn accept_str_and_string<S: Into<String>>(value: S) {
    let string_value: String = value.into();
    println!("string_value: {}", string_value);
}

fn main() {
    let str_foo = "foo";
    let string_foo = String::from("foo");

    accept_str_and_ref_string(str_foo);
    accept_str_and_ref_string(&string_foo);

    accept_str_and_string(str_foo);
    accept_str_and_string(string_foo);
}

Is it possible to implement one function so that I can do this:

accept_all_strings(str_foo);
accept_all_strings(&string_foo);
accept_all_strings(string_foo);
Geelong answered 9/3, 2019 at 15:50 Comment(2)
It looks like since Rust 1.35.0 the Into<String> variant also works for &String.Larondalarosa
Yes, you're right, I've testet it with the current 1.36 version of Rust.Geelong
M
73

You can use the AsRef<str> trait:

// will accept any object that implements AsRef<str>
fn print<S: AsRef<str>>(stringlike: S) {
    // call as_ref() to get a &str
    let str_ref = stringlike.as_ref();

    println!("got: {:?}", str_ref)
}

fn main() {
    let a: &str = "str";
    let b: String = String::from("String");
    let c: &String = &b;

    print(a);
    print(c);
    print(b);
}

The print function will support any type that implements AsRef<str>, which includes &str, String and &String.

Moralize answered 9/3, 2019 at 15:58 Comment(4)
@trentcl A String will not be copied, the ownership of it will be passed to the method unless you are using a reference.Moralize
My mistake, my brain mashed up the body of the OP's example with the signature of yours.Manor
What if the function eventually needs an owned string rather than a reference?Alexipharmic
@Alexipharmic Personally I typically just use stringlike.as_ref().to_string(), as I like having the same signature regardless of whether I need an owned or borrowed string. I think that might be a bit inefficient with arguments that are already owned strings, so if you want to avoid that you can use other traits, like S: Into<String>.Moralize
D
20

Since the 2015 rust edition the accept_all_strings function works as required by the OP with the Into<String> trait:

fn accept_all_strings<S: Into<String>>(value: S) {
    let string_value = value.into();
    println!("string_value: {string_value}");
}

Thanks to Jeremy Chone for the tip. The function signature can now be shortened like this:

fn accept_all_strings(value: impl Into<String>) {
    let string_value = value.into();
    println!("string_value: {string_value}");
}

Playground

A really good overview of conversions between String, &str, Vec<u8> and &[u8] can be found here…

Dialogize answered 30/8, 2022 at 20:37 Comment(8)
Does exactly what the OP wanted: Playground – so WTF?Dialogize
Yes, this function accepts the 3 types of strings and does allocation only when it has to. It seems OP had put this option accept_str_and_string and perhaps thought you could not pass a &String. Btw, now, we can do print(stringlike: impl Into<String>) { ... } which is a little shorter.Kreiker
@Chone You mention the new ability of the print macros to capture the argument directly from a variable? I took the OP's request literally that he wants the accept_all_strings function. IMHO: To name an user function print is misleading and error prone. And I'm not sure if choosing Into over AsRef is just a matter of taste…Dialogize
Sorry, my mistake, I did not mean to refer to the print macro. Should be accept_all_strings(val: impl Into<String>) as you noted.Kreiker
I think there is a difference between AsRef, and Into. The former can only get a reference; you must do a new allocation if you want ownership. The Into will give you ownership without allocation when possible. I tend to use the Into for my builders/struct, as most of the time, I want ownership of the String, but do not want to require the caller to add .to_string() on refsKreiker
But value.into() will create/allocate a new String even if the function got a &str, right? (I guess it is unnecessary)Fullmer
@Fullmer Besides printing an unmodified string, do you know of a second use case, where you don't need the string as owned?Dialogize
@Kaplan, Yes, if I want to parse / find something in the input str/String, and return just that number/part.Fullmer
S
5

Both Into<String> and AsRef<str> (as described in the other answers) will accept String, &str, and &String. They are different and which one to use will depend on your situation.

If you need an owned string then use Into<String>

Using AsRef<str> here will rob your caller of the opportunity to avoid a string copy

struct Favorites {
    things: Vec<String>
}

impl Favorites {
    pub fn add_thing(&mut self, thing: impl Into<String>) {
        self.things.push(thing.into())
    }
}

fn main() {
    let mut favs = Favorites { things: Vec::new() };
    // String literals are static references and we can't ever get
    // the owned value so a clone of the bytes is necessary
    favs.add_thing("hi"); // Contents cloned implicitly

    // std::env::var already makes a new string.  We avoid
    // cloning this value needlessly because we used Into<String>
    let my_fav = std::env::var("USER_FAVORITE");
    if let Ok(my_fav) = my_fav {
        favs.add_thing(my_fav); // No clone
    }
        
    // Note, there is a `impl From<&String> for String` which
    // makes a sneaky clone.  Watch out for this
    let my_fav = std::env::var("USER_FAVORITE");
    if let Ok(my_fav) = my_fav {
        favs.add_thing(&my_fav); // Contents cloned implicitly
    }
}

If you just need a string reference then use AsRef<str>

Using Into<String> here will force a string copy where none is needed

fn count_whitespace(value: impl AsRef<str>) -> usize {
    value.as_ref().chars().filter(|c| c.is_whitespace()).count()
}

fn main() {
    let my_str = "he llo ";
    // Once clone of my_str is made here
    let my_string = my_str.to_string();
    
    // No clone of string data is made beyond this point
    let num_ws_2 = count_whitespace(&my_string);
    let num_ws = count_whitespace(my_string);
    let num_ws_3 = count_whitespace(my_str);
    dbg!(num_ws, num_ws_2, num_ws_3);
}
Sophrosyne answered 8/12, 2023 at 23:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.