Should functions operate on String or &str?

Should common functions operate on String or &str? Is there any performance difference at all?

e: I'm not going to mutate the string.

In most cases I think it's better to accept &str as a fn parameter which allows you to pass a borrowed String or a &str because of deref coercion.

Read this String vs &str in Rust functions

That's a very good link :slight_smile:

I think it should appear in Rust By Example :wink:

There's a case, where accepting String is more efficient:
If your methods needs an owned string, e.g. to store it in a struct. That way the caller has the possibility to move an existing String instance that is not needed anymore.

E.g:

struct Person {
    name: String,
}

impl Person {
    fn set_name(&mut self, name: String) {
        self.name = name;
    }
}

fn main() {
    let name: String = ...;
    // Do something with the string...
    person.set_name(name); // No additional allocation is needed 
                           // because the existing string is moved
}

There are two traits that may be of use here: AsRef and Into. They may or may not be applicable depending on your situation. Here are some examples:

fn take_owned<T: Into<String>>(s: T) {
    println!("String: {}", s.into());
}

fn take_ref<T: AsRef<str>>(s: T) {
    println!("String: {}", s.as_ref());
}

fn main() {
    let s = "Rust Forum".to_string();
    
    take_ref(&s);
    take_ref("Rust Forum [static]");
    take_owned(s);
    take_owned("Rust Forum [static]");
    
    // s is no longer accessible
}

I took troplin's example one step further to show you how Into could help:

struct Person {
    name: Option<String>,
}

impl Person {
    fn name(&self) -> Option<&str> {
        if let Some(ref name) = self.name {
            Some(name)
        } else {
            None
        }
    }
    
    fn set_name<T: Into<String>>(&mut self, name: T) {
        self.name = Some(name.into());
    }
}

fn main() {
    let mut p = Person{name: None};
    let s = String::from("Heap Name");
    
    p.set_name(s);
    println!("Person: {}", p.name().unwrap());
    
    p.set_name("Static Name");
    println!("Person: {}", p.name().unwrap());
    
    // s is no longer accessible
}

That's true but it also makes the function signatures considerably more complex.

In this case, the additional complexity is pretty limited. There's definitely a trade though - ergonomics and generality for everyone versus simpler function signatures which cater to beginners.

IMO the difference between a plain function and a generic function is quite big. (Although that distinction is a bit blurred in Rust because of lifetimes and lifetime elision).

I don't think that the distinction "beginners vs. everyone" holds here. Generally, those kind of signatures are pure convenience, they don't really make the function more general. Calling into() in the caller is essentially the same, just without hiding the complexity.

In my taste, Rust tends to go a bit overboard with genericity. For example take Iterator::collect, it's defined as:

pub trait Iterator {
    type Item;
    //...
    fn collect<B>(self) -> B where B: FromIterator<Self::Item> { ... }
   // ...
}

it uses the FromIterator trait, which is defined as:

pub trait FromIterator<A> {
    fn from_iter<T>(iter: T) -> Self where T: IntoIterator<Item=A>;
}

which itself uses the IntoIterator trait:

pub trait IntoIterator where Self::IntoIter::Item == Self::Item {
    type Item;
    type IntoIter: Iterator;
    fn into_iter(self) -> Self::IntoIter;
}

I find this hard to wrap my head around, even if I'm generally familiar with the language. There's not a single concrete type in those signatures, only traits and associated types.

2 Likes

How about a Cow ?

1 Like