Should I write string function arguments as &str, String, AsRef<str>, or Into<String> in Rust?

Asked 5 days ago Modified 5 days ago Viewed 14 times

I'm developing a public Rust library and I'm unsure how to handle function parameters that deal with strings.

There seem to be multiple idiomatic options:

  • &str
  • String
  • impl AsRef<str>
  • impl Into<String>

Each of these works in different cases, but I don't understand when to use one over the others, especially when weighing ergonomics and API design vs. performance.

For example, if I have:

fn greet(name: &str) { ... }
fn greet<T: AsRef<str>>(name: T) { ... }
fn greet<T: Into<String>>(name: T) { ... }
fn greet(name: String) { ... }

Which case is more idiomatic, and in what situations?

2
1 Answer
Sort by
Posted 2 days ago Modified 2 days ago

&str - String Slice

Use this when you need to read the string (or parts of it), but don't need to own it.

let slice = "bob";
greet_str(slice);
let owned = String::new("bob");
greet_str(&owned); // Requires explicit borrowing of `owned`

fn greet_str(name: &str) {
    println!("{}", name);
}

Pros:

  • Fast, no allocation or transfer
  • Explicit

Cons:

  • Caller must explicitly pass a reference (greet_str(owned) will throw a compile-time error)
  • You can't take ownership of the input

String - Owned String

Use this when you need to write or take ownership of the string, want to give power to the caller on how and when the string is allocated, and want to be explicit.

let slice = "bob";
greet_string(slice.to_string()); // Requires explicit allocation of `slice`
let owned = String::new("bob");
greet_string(owned); // Takes ownership of `owned`

fn greet_string(name: String) {
    println!("{}", name);
}

Pros:

  • Clear signal of intent that the function is taking ownership
  • No cloning needed if caller already has a String

Cons:

  • Slightly less ergonomic for callers: they must allocate if passing a &str and clone if they want to keep an owned copy

AsRef<str> - Flexible for Borrowing

Use this when you want to accept anything that borrows as a string (implements AsRef<str>), and you don't need to take ownership.

let slice = "bob";
greet_asref(slice);
let owned = String::new("bob");
greet_asref(owned); // Notice how we don't have to explicitly borrow `owned`

fn greet_asref<T: AsRef<str>>(name: T) {
    println!("Hello {}", name.as_ref())
}

Pros:

  • Accepts both &str, String and other string-like types (e.g. Cow<'_, str>) without the need for explicit dereferencing

Cons

  • Slightly less obvious what the functions expects
  • Can't take direct ownership of the argument, just borrows

impl Into<String> - Flexible for Ownership

Use this when you need to take ownership of the string but want to be flexible about what can be passed to the function (anything that implements Into<String>).

let slice = "bob";
greet_into(slice); // The allocation/cloning is done for us inside the function when `.into()` is called
let owned = String::new("bob");
greet_into(owned); // Since we're passing ownership to the function, the string will not be reallocated inside the function

fn greet_into<T: Into<String>>(name: T) {
    let owned_name = name.into(); // This will only allocate a new string if `name` isn't owned

    println!("Hello {}", owned_name);
}

Pros:

  • Accepts both String and &str, allocating a String for &str automatically

Cons:

  • Always incurs an allocation if the input is a borrowed type (e.g. &str)
  • Less explicit than asking for String directly
  • Can result in callers inadvertently causing a clone: greet_into(String::new("bob").as_str())

Conclusion

There isn't a definitive catch-all answer to this question, it really is up to your specific requirements. If you want to be more explicit about what your functions expect, &str and String are probably more appropriate for you. However, if flexibility and ergonomics are a priority, AsRef<str> and Into<String> are also suitable.

There shouldn't be a major difference in performance between accepting the trait vs. concrete types. Where performance issues could arise at scale is if the caller mistakenly clones or causes a clone when it's not necessary (I'd imagine this would be most common with Into<String>). However, at a small-scale (not inside loops) and in a non-embedded environment, a few accidental clones likely won't cause issues.

1
J
J
99

Your Answer

You Must Log In or Sign Up to Answer Questions