Definitions
Let's start with definitions
String
is a growable buffer that owns it's data.
&String
is a shared borrow of a growable buffer, this borrow does not own any data
&mut String
is a unique borrow of a growable buffer, this borrow does not own any data
str
is a view into some other string that is already in memory.
&str
is a shared borrow of a string view.
&mut str
is a unique borrow of a string view.
String literals
String literals, like "hello world"
or "bob"
are stored directly into the program binary, and we interact with a shared borrow of a string view to that string. Which, if we look at are definitions above is &str
. Only thing missing is the lifetime, because the literals are stored in the program binary, they have a 'static
lifetime. Put all of this together and we get &'static str
.
Creating a String
You can create a string using some traits that str
and String
implement ToString::to_string
or From<&str>
. ToString
is implemented on every type that implements Display
, so that you can easily create String
from those types. Because str
and Srring
implement Display
you get to use ToString
. String
also implements From<&str>
because it can be losslessly converted from a &str
in an obvious way.
Less commonly, you may see people use ToOwned
to create a String
. str: ToOwned<Owned = String>
, so you can use string_view.to_owned()
to get a String
.
Because String: From<&str>
you also get Into<String> for &str
for free, so you can do string_view.into()
to get a string. But this isn't all that great because Rust has a hard time doing type inference with the Into
trait, so I would stick to to_string
, to_owned
, or String::from
.
Indexing
String
and str
can be indexed to produce a string view into their contents using ranges. Like so
let string: String = String::from("Hello World");
let view: str = string[..];
Oh no, this doesn't compile, why?
Well, if we look at our definitions from before we can see that str
is a view into some other data. But how big is that view? Can't be known until run-time. So str
does not implement Sized
. This means that it must always be behind some indirection, like a borrow (&
or &mut
).
let string: String = String::from("Hello World");
let view: &str = &string[..]; // fixed
Note about your println example, you added a &
on your own, so it works. Not magic on the println macro's end.
Deref Coercions
Before we dive into this with String
and str
, lets look at a simpler example
use std::ops::Deref;
struct Foo;
struct Bar(Foo);
impl Deref for Bar {
type Target = Foo;
fn deref(&self) -> &Foo {
&self.0
}
}
fn main() {
let bar : Bar = Bar(Foo);
let bar_borrow : &Bar = &bar;
let foo_borrow : &Foo = bar_borrow;
let foo_borrow : &Foo = &bar;
}
Witchcraft. How are we converting between types! This has to do with the so called deref coercions. Basically if Rust thinks that you have mismatched borrow types, Rust will try and apply a special coercion, which depends on the Deref
and DerefMut
traits. This conversion will convert the borrows to borrows of Deref::Target
.
String
implements Deref<Target = str>
, so it can participate in these coercions.
fn main() {
let string : String = String::from("Hello World");
let string_borrow : &String = &string;
let view_borrow : &str = string_borrow;
let view_borrow : &str = &string;
}
Book Suggestion
The book suggests always taking &str
for function arguments because it gives you more ergonomics are flexibility, and I will add only one caveat, if you are going to be storing a String
then take a String
to avoid unnecessary allocations.
Cow<'a, str>
On the topic of functions, if you are unsure that you will allocate (due to conditional allocations or something similar), and you would like to avoid unnecessary allocations you can use the Cow<'a, str>
type. This type can represent both a string view and an owned string.
C++
This is similar to the distinction between string_view
and string
in C++, which correspond to &str
and String
in Rust respectively.